Skip to main content

Rust for Functions

You can write your functions in Rust. This guide describes the shopify_function Rust crate that Shopify provides to help developers build with Shopify Functions.


  • The latest stable version of Rust, with the wasm32-unknown-unknown build target installed:

    Terminal

    rustup target add wasm32-unknown-unknown
  • Version 3.85.0 or higher of Shopify CLI. Shopify recommends using the latest version.

  • Version 2.0.0 or higher of the shopify_function crate. Functions that you generate with Shopify CLI use the latest version.


The shopify_function Rust crate performs type generation, reduces boilerplate code, and makes it easier to test various function inputs. It includes the following components:

ComponentDescription
typegenA macro to enable struct generation from the Function API, based on the provided GraphQL schema and input query.
shopify_functionAn attribute macro that marks the given function as the entrypoint for Shopify Functions, by:
  • Exporting a WebAssembly function with the same name as the Rust function. This name must match the target's export in the function extension configuration.
  • Reading the function's input and writing its output through the Shopify Functions WebAssembly API.
DeserializeA derive macro that deserializes function input into your own structs, such as a JSON metafield configuration. It supports the rename_all, rename, and default attributes through #[shopify_function(...)].
log!A macro that writes to your function's logs. Use it instead of eprintln!.
run_function_with_inputA utility for unit testing that enables you to add new tests based on a given JSON input string.

Anchor to Viewing the generated typesViewing the generated types

To preview the types generated by the typegen macro, use the cargo doc command.

Terminal

cargo doc --open

You can also use the cargo-expand crate to view the generated source:

Terminal

cargo install cargo-expand
cargo expand

To make development easier, install the rust-analyzer VSCode extension for:

  • Code completion
  • Go to definition
  • Real-time error checking
  • Type information on hover

Anchor to Example implementationsExample implementations

Explore example implementations using the shopify_function Rust crate.


The compiled Wasm file for a Shopify Function must be under 256 kB. Here are a few tips to keep binary size small when using Rust:

  • Update the shopify_function crate to the latest version.

  • Keep the release profile settings from the function templates in your Cargo.toml. They enable link-time optimization, optimize for size, and strip symbols:

    Cargo.toml

    [profile.release]
    lto = true
    opt-level = "z"
    strip = true
  • Keep the wasm_opt configuration property enabled. Shopify CLI runs wasm-opt on your module by default, which further reduces its size.

  • For regular expressions, use the regex_lite crate.

  • Follow tips and documentation in the johnthagen/min-sized-rust GitHub repository.

  • Use to_ascii_uppercase and to_ascii_lowercase when possible to avoid pulling in Unicode tables, unless needed.

  • Only query for data you need.

    Code generation happens for all types and fields included in the input queries (for example, run.graphql). Review and remove any unused parts of the queries.

  • Keep JSON metafields that require deserialization as small as possible.

    Code generated for deserialization increases the binary size. The smaller the metafield is, the less code needs to be generated.

To reduce the number of instructions that your function executes, refer to Optimize instruction counts.


Anchor to Updating existing function to using shopify_function 2.0.0 and higherUpdating existing function to using shopify_function 2.0.0 and higher

If you are using a version less than 1.0.0, you should update to version 1.1.1 as outlined below before following these steps.

  1. Update to the latest Shopify CLI version. Version 2.0.0 of the crate requires Shopify CLI 3.85.0 or higher.

  2. Update the shopify_function dependency in your Cargo.toml to the latest version:

    Cargo.toml

    [dependencies]
    shopify_function = "2.2.0"
  3. Install the wasm32-unknown-unknown build target using rustup target:

    Terminal

    rustup target add wasm32-unknown-unknown
  4. Update your build command and path in the [extensions.build] section of your shopify.extension.toml to use wasm32-unknown-unknown instead of wasm32-wasip1. Replace RUST-PACKAGE-NAME with the name from your Cargo.toml:

    shopify.extension.toml

    [extensions.build]
    command = "cargo build --target=wasm32-unknown-unknown --release"
    path = "target/wasm32-unknown-unknown/release/[RUST-PACKAGE-NAME].wasm"
  5. Throughout all of your source files, update any references to eprintln! to use log! instead.

    #[shopify_function]
    fn run(input: schema::run::Input) -> Result<schema::FunctionRunResult> {
    log!("This will be logged");
    todo!();
    }
  6. Throughout all of your source files, update any references to process::exit(1) to use process::abort() instead.

    #[shopify_function]
    fn run(input: schema::run::Input) -> Result<schema::FunctionRunResult> {
    log!("Please invoke a named export.");
    process::abort();
    }

Anchor to Updating existing function to using shopify_function 1.0.0 and higherUpdating existing function to using shopify_function 1.0.0 and higher

If your function uses a version of the shopify_function crate below 1.0.0, then follow these steps to update it to version 1.0.0 and higher. After that, update to version 2.0.0 and higher.

  1. In main.rs, add imports for shopify_function.

    use shopify_function::prelude::*;
    use shopify_function::Result;
  2. In main.rs, add type generation, right under your imports. Remove any references to the generate_types! macro.

    #[typegen("schema.graphql")]
    pub mod schema {
    #[query("src/run.graphql")]
    pub mod run {}
    }

    If your Function has multiple targets each with their own input query, add a nested module for each. For example:

    #[typegen("schema.graphql")]
    pub mod schema {
    #[query("src/fetch.graphql")]
    pub mod fetch {}

    #[query("src/run.graphql")]
    pub mod run {}
    }
  3. In main.rs, ensure that you have a main function that returns an error indicating to invoke a named export:

    fn main() {
    log!("Please invoke a named export.");
    std::process::abort();
    }
  4. If you have an input query to retrieve a JSON metafield value in your run.graphql file, for example:

    Rust input query

    src/run.graphql
    query Input {
    deliveryCustomization {
    metafield(namespace: "delivery-customization", key: "function-configuration") {
    jsonValue
    }
    }
    }

    You can deserialize the jsonValue directly into an object you define in your run.rs file and annotate with #[shopify_function(rename_all = "camelCase")] and #[derive(Deserialize)] as shown below:

    Rust

    src/run.rs
    #[derive(Deserialize)]
    #[shopify_function(rename_all = "camelCase")]
    pub struct DeliveryConfiguration {
    state_province_code: String,
    message: String,
    }

    Finally, use custom_scalar_overrides to link the jsonValue with its object definition in your main.rs file as shown below:

    Rust

    src/main.rs
    #[typegen("schema.graphql")]
    mod schema {
    #[query("src/run.graphql",
    custom_scalar_overrides = {
    "Input.deliveryCustomization.metafield.jsonValue" => super::run::DeliveryConfiguration,
    }
    )]
    pub mod run {}
    }
  5. Ensure your source file that has the function logic defined, includes the following imports.

    use shopify_function::prelude::*;
    use shopify_function::Result;
    use super::schema;

    typically this is in run.rs or fetch.rs

  6. Throughout all of your source files, replace any references to #[shopify_function_target] with the #[shopify_function] macro, and change its return type. Typically, this is located in a file with a name equal to the target, e.g. run.rs.

    #[shopify_function]
    fn run(input: schema::run::Input) -> Result<schema::FunctionRunResult> {
  7. Update the types and fields utilized in the function to the new, auto-generated structs. For example:

    OldNew
    input::ResponseDataschema::run::Input
    input::InputDiscountNodeMetafieldschema::run::input::discount_node::Metafield
    input::InputDiscountNodeschema::run::input::DiscountNode
    output::FunctionRunResultschema::FunctionRunResult
    output::DiscountApplicationStrategy::FIRSTschema::DiscountApplicationStrategy::First

Anchor to Updating to Rust 1.84 and higherUpdating to Rust 1.84 and higher

Previously, we encouraged the use of cargo-wasi as a way to build and optimize your Rust functions. However, as of Rust version 1.84, the wasm32-wasi build target used by cargo-wasi was removed.

If your function still builds with cargo-wasi or the wasm32-wasi target, then update it to version 2.0.0 or higher of the shopify_function crate by following the steps in Updating existing function to using shopify_function 2.0.0 and higher. Version 2.0.0 and higher builds with the wasm32-unknown-unknown target, and works with the latest stable version of Rust.

You can also remove the deprecated wasm32-wasi build target using rustup target:

Terminal

rustup target remove wasm32-wasi
Note

In addition to building your Rust function for WebAssembly, the cargo-wasi crate also optimized the size of your binary using the Binaryen toolchain. Shopify CLI optimizes your module by default. You can configure this behavior with the wasm_opt configuration property.


Anchor to Migrating from JavaScriptMigrating from JavaScript

Migrating your JavaScript Shopify Function to Rust can significantly improve performance and help you stay within platform fuel limits. Rust compiles directly to WebAssembly, resulting in more efficient execution compared to JavaScript. To measure and reduce instruction counts before and after migration, refer to Optimize instruction counts.

The safest way to migrate is to add your Rust code alongside your JavaScript code in the same function extension, and then use shopify.extension.toml to choose which language to build. Your extension, its uid, and its configuration stay the same. Only the language and the compiled WebAssembly module change, so merchants don't need to do anything, and you can switch back to JavaScript at any time.

Anchor to Add Rust to your JavaScript functionAdd Rust to your JavaScript function

  1. Update your JavaScript function to version 2.0.0 or higher of the @shopify/shopify_function package. While your JavaScript entry file is in src/, Shopify CLI picks the local function runner based on this package's version. Earlier versions use a function runner that can't run modules built with version 2.0.0 or higher of the shopify_function Rust crate, so app function run, app function replay, and profiling fail for your Rust build. Run the following command in your function's directory, and then confirm that your JavaScript function still builds and passes its tests:

    Terminal

    npm install @shopify/shopify_function@^2.0.0
  2. Install the wasm32-unknown-unknown build target using rustup target:

    Terminal

    rustup target add wasm32-unknown-unknown
  3. Generate a temporary Rust function to use as a starting point:

    Terminal

    shopify app generate extension

    When prompted, choose the same function type as your existing JavaScript function, and select Rust as the language.

  4. Copy the Cargo.toml file from the temporary function into your JavaScript function's directory.

  5. Copy the .rs files from the temporary function's src/ directory into your JavaScript function's src/ directory. Your JavaScript files stay where they are, and both languages share the same schema.graphql file and input queries.

  6. Delete the temporary function's directory, so that it isn't deployed with your app.

  7. Add target/ to your function's .gitignore file to exclude Rust build output.

  8. Update your input queries for Rust type generation. Add __typename to any fragments on interfaces or unions. This change doesn't affect your JavaScript code:

    src/cart_lines_discounts_generate_run.graphql

    query CartInput {
    cart {
    lines {
    merchandise {
    __typename
    ... on ProductVariant {
    id
    }
    }
    }
    }
    }

    You don't need to rename your queries. The Rust input type takes its name from the query's operation name, so query CartInput generates a CartInput type. Update the copied Rust code to use your type names, and make sure that the #[query] paths in main.rs match your input query files.

  9. If your function reads its configuration from a JSON metafield, then deserialize the metafield directly into a Rust struct. Your JavaScript function reads the parsed object from jsonValue. In Rust, you define a struct for the configuration and tell the typegen macro to use it as the type of the jsonValue field.

    For example, the following input query reads a JSON configuration metafield on the discount:

    src/cart_lines_discounts_generate_run.graphql

    query CartInput {
    discount {
    metafield(namespace: "$app:my-discount", key: "function-configuration") {
    jsonValue
    }
    }
    }

    Define a struct that matches your configuration JSON. The Deserialize derive macro from the shopify_function crate reads the value directly from the function input. Use rename_all = "camelCase" to map the JSON keys to snake_case field names, and default for optional fields:

    src/cart_lines_discounts_generate_run.rs

    use shopify_function::prelude::*;

    #[derive(Deserialize)]
    #[shopify_function(rename_all = "camelCase")]
    pub struct Configuration {
    pub discount_percentage: f64,
    #[shopify_function(default)]
    pub excluded_product_ids: Vec<String>,
    }

    In main.rs, use the custom_scalar_overrides argument of the #[query] attribute to map the jsonValue field to your struct. The key is the path to the field, starting with the query's operation name. A type path that doesn't start with :: is relative to the schema module, so use super:: to reach your target's module:

    src/main.rs

    #[typegen("schema.graphql")]
    pub mod schema {
    #[query(
    "src/cart_lines_discounts_generate_run.graphql",
    custom_scalar_overrides = {
    "CartInput.discount.metafield.jsonValue" => super::cart_lines_discounts_generate_run::Configuration,
    }
    )]
    pub mod cart_lines_discounts_generate_run {}
    }

    Your function then receives the configuration as a typed struct, with no JSON parsing in your code:

    src/cart_lines_discounts_generate_run.rs

    let Some(configuration) = input.discount().metafield().map(|metafield| metafield.json_value()) else {
    return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] });
    };
    let percentage = configuration.discount_percentage;
  10. Port your JavaScript logic to the .rs files.

Anchor to Switch between JavaScript and RustSwitch between JavaScript and Rust

The [extensions.build] section and each target's export in shopify.extension.toml determine which language Shopify CLI builds. Rust exports use the name of the Rust function marked with #[shopify_function], which is usually in snake_case, so the export values differ between languages.

To build the Rust version, update the export for each target and the [extensions.build] section. Keep your JavaScript values as comments. To switch back, comment out the Rust lines, including watch, and uncomment the JavaScript lines. Replace RUST-PACKAGE-NAME with the name from your Cargo.toml:

shopify.extension.toml

[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
# JavaScript:
# export = "cart-lines-discounts-generate-run"
# Rust:
export = "cart_lines_discounts_generate_run"

[extensions.build]
# JavaScript:
# command = ""
# path = "dist/function.wasm"
# Rust:
command = "cargo build --target=wasm32-unknown-unknown --release"
path = "target/wasm32-unknown-unknown/release/[RUST-PACKAGE-NAME].wasm"
watch = ["src/**/*.rs"]
Note

While your JavaScript entry file, such as src/index.js, is in the src/ directory, Shopify CLI treats the extension as a JavaScript function in some places. For example, app function run --profile warns that the profile won't contain function names, even when your Rust build keeps them.

Anchor to Validate and deploy the migrationValidate and deploy the migration

  1. Update your integration tests so that the same fixtures work with both languages. Each fixture's export value records the JavaScript export name, which the Rust module doesn't have. In tests/default.test.js, pass the export from shopify.extension.toml to runFunction instead:

    tests/default.test.js

    const runResult = await runFunction(
    { ...fixture, export: targeting[fixture.target].export ?? fixture.export },
    functionRunnerPath,
    wasmPath,
    targetInputQueryPath,
    schemaPath,
    );
  2. Run your function's integration tests with the JavaScript configuration, and then again with the Rust configuration. Both versions should produce the expected output for every fixture. You can also compare instruction counts between the two runs:

    Terminal

    npm test
  3. Deploy to a dev store and verify that the function works as expected.

  4. Deploy your app. Every store that uses your function runs the Rust version, because the extension itself hasn't changed.

  5. If you find an issue, then switch shopify.extension.toml back to the JavaScript configuration and deploy again.

  6. When you're confident in the Rust version, remove your JavaScript source files from src/.



Was this page helpful?