Skip to main content

Create an admin UI extension for a cart and checkout validation function

To ensure that purchases meet certain criteria before customers can complete an order, you can use he Cart and Checkout Validation Function API and an admin UI extension.

In this tutorial, you’ll use Shopify Functions to enforce product limits on store merchandise.

Note

Errors from validation functions are exposed to the Storefront API's Cart object, in themes using the cart template, and during checkout.

A checkout with an error about a product quantity that is too high

In this tutorial, you’ll learn how to do the following tasks:

  • Generate starter code for Shopify Functions.
  • Use GraphQL to define the input of your function.
  • Deploy functions to the Shopify platform.
  • Review logs for your function.
  • Create an admin UI extension to configure your function.

Tip

Shopify defaults to Rust as the most performant and recommended language choice to stay within the platform limits. For more information, refer to language considerations.

  • You're using API version 2025-07 or higher for your function.

Anchor to Rust-specific requirementsRust-specific requirements

The following requirements are specific to Rust-based development with Shopify Functions.

  • You've installed Rust.

    On Windows, Rust requires the Microsoft C++ Build Tools. Make sure to select the Desktop development with C++ workload when installing the tools.

  • You've installed the wasm32-wasip1 target:

    Terminal

    rustup target add wasm32-wasip1

Anchor to Step 1: Create the validation functionStep 1: Create the validation function

To create a validation function, use Shopify CLI to generate a starter function, specify the inputs for your function using an input query, and implement your function logic using JavaScript or Rust.

  1. Navigate to your app directory:

    Terminal

    cd <directory>
  2. Run the following command to create a new validation extension:

    Terminal

    shopify app generate extension --template cart_checkout_validation --name cart-checkout-validation
Tip

Shopify Functions support any language that compiles to WebAssembly (Wasm), such as Rust, AssemblyScript, or TinyGo. You specify the Wasm template option when you're using a language other than Rust and can conform to the Wasm API. Learn more about the Wasm API.

  1. Choose the language that you want to use. For this tutorial, you should select either Rust or JavaScript.

    Shopify defaults to Rust as the most performant and recommended language choice to stay within the platform limits. For more information, refer to language considerations.

    Terminal

    ? What would you like to work in?
    > (1) Rust
    (2) JavaScript
    (3) TypeScript
    (4) Wasm
  1. Navigate to the extensions/cart-checkout-validation directory:

    Terminal

    cd extensions/cart-checkout-validation
  2. Replace the contents of src/cart_validations_generate_run.graphql file with the following code. The cart_validations_generate_run.graphql file defines the input for the function. You need to retrieve the quantity and merchandise ID of the current cart lines.

    Metafields allow your app to store custom data related to the validation function. Using the $app reserved prefix makes the metafield private to your app.

    Note

    The query differs slightly in Rust and JavaScript due to code generation requirements.

    src/cart_validations_generate_run.graphql

    src/cart_validations_generate_run.graphql

    query Input {
    cart {
    lines {
    quantity
    merchandise {
    __typename
    ... on ProductVariant {
    id
    product {
    title
    }
    }
    }
    }
    }
    validation {
    metafield(namespace: "$app:product-limits", key: "product-limits-values") {
    jsonValue
    }
    }
    }
    query CartValidationsGenerateRunInput {
    cart {
    lines {
    quantity
    merchandise {
    __typename
    ... on ProductVariant {
    id
    product {
    title
    }
    }
    }
    }
    }
    validation {
    metafield(namespace: "$app:product-limits", key: "product-limits-values") {
    jsonValue
    }
    }
    }
  3. If you're using JavaScript, then run the following command to regenerate types based on your input query:

    Terminal

    shopify app function typegen
  4. If you're using Rust replace the src/main.rs file with the following code that will convert the metafield into a data structure in the Rust program.

    main.rs

    src/main.rs
    use shopify_function::prelude::*;
    use std::process;

    pub mod run;

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

    fn main() {
    eprintln!("Please invoke a named export.");
    process::exit(1);
    }
  5. Replace the src/cart_validations_generate_run.rs or src/cart_validations_generate_run.js file with the following code. The function logic checks that the quantity of each cart line isn't above the quantity set in the configuration metafield. You can configure the quantity limits for each product variant using the admin UI extension that you will create in step 2.

    Tip

    You can associate a validation error with a specific checkout UI field, or a global error by specifying the target property. The target property follows the pattern that's provided in the Cart and Checkout Validation API reference. For example, using the global target $.cart will result in a global error at the top of checkout.

    File

    src/cart_validations_generate_run.rs

    use shopify_function::prelude::*;
    use shopify_function::Result;
    use super::schema;
    use std::collections::HashMap;

    #[derive(Deserialize, Default, PartialEq)]
    pub struct Configuration {
    limits: HashMap<String, i32>
    }

    #[shopify_function]
    fn run(input: schema::run::Input) -> Result<schema::CartValidationsGenerateRunResult> {
    let mut operations = Vec::new();
    let mut errors = Vec::new();

    let configuration = if let Some(metafield) = input.validation().metafield() {
    metafield.json_value()
    } else {
    return Ok(schema::FunctionRunResult { errors: vec![] });
    };

    input
    .cart()
    .lines()
    .iter()
    .for_each(|line| {
    let quantity = line.quantity();
    match &line.merchandise() {
    schema::run::input::cart::lines::Merchandise::ProductVariant(variant) => {
    let limit = configuration.limits.get(variant.id()).unwrap_or(&i32::MAX);
    let product_name = variant.product().title();

    // Check item quantity in the cart against the configured limit
    if quantity > limit {
    errors.push(schema::ValidationError {
    message: format!("Orders are limited to a maximum of {} of {}", limit, product_name),
    target: "cart".to_owned(),
    });
    }
    },
    _ => {},
    };
    });

    let operation = schema::ValidationAddOperation { errors };
    operations.push(schema::Operation::ValidationAdd(operation));
    Ok(schema::FunctionRunResult { errors })
    }
    // @ts-check

    /**
    * @typedef {import("../generated/api").CartValidationsGenerateRunInput} CartValidationsGenerateRunInput
    * @typedef {import("../generated/api").CartValidationsGenerateRunResult} CartValidationsGenerateRunResult
    */

    /**
    * @param {CartValidationsGenerateRunInput} input
    * @returns {CartValidationsGenerateRunResult}
    */
    export function cartValidationsGenerateRun({ cart, validation }) {
    // Read persisted data about product limits from the associated metafield
    /** @type {Array<{productVariantId: string; quantityLimit: number}>} */
    const configuration = validation.metafield?.value ?? {};
    const errors = [];

    for (const { quantity, merchandise } of cart.lines) {
    if ("id" in merchandise) {
    const limit = configuration[merchandise.id] ?? Infinity;
    const title = merchandise.product.title || "Unknown product";

    // Check item quantity in the cart against the configured limit
    if (quantity > limit) {
    errors.push({
    message: `Orders are limited to a maximum of ${limit} of ${title}`,
    target: "cart",
    });
    }
    }
    }

    const operations = [
    {
    validationAdd: {
    errors
    },
    },
    ];

    return { operations };
    }
  6. If you're using Rust, then build the function's Wasm module:

    Terminal

    cargo build --target=wasm32-wasip1 --release

    If you encounter any errors, then ensure that you've installed Rust and the wasm32-wasip1 target.


Anchor to Step 2: Create the validation user interface in adminStep 2: Create the validation user interface in admin

The following steps show how to build an admin UI extension that enables merchants to configure a validation function.

Admin UI extension for configuring the validation function
  1. Navigate to your app directory:

    Terminal

    cd <directory>
  2. Run the following command to create a new validation rule UI extension:

    Terminal

    shopify app generate extension --template validation_settings_ui --name validation-settings
  3. Choose the language that you want to use.

    Terminal

    ? What would you like to work in?
    > (1) JavaScript React
    (2) JavaScript
    (3) TypeScript React
    (4) TypeScript
  4. Navigate to the extensions/validation-settings directory:

    Terminal

    cd extensions/validation-settings
  5. Replace the validation settings UI code with the following code:

    File

    src/ValidationSettings.jsx

    import React, { useState } from "react";
    import {
    reactExtension,
    useApi,
    Text,
    Box,
    FunctionSettings,
    Section,
    NumberField,
    BlockStack,
    Banner,
    InlineStack,
    Image,
    } from "@shopify/ui-extensions-react/admin";

    const TARGET = "admin.settings.validation.render";

    export default reactExtension(TARGET, async (api) => {
    const existingDefinition = await getMetafieldDefinition(api.query);
    if (!existingDefinition) {
    // Create a metafield definition for persistence if no pre-existing definition exists
    const metafieldDefinition = await createMetafieldDefinition(api.query);

    if (!metafieldDefinition) {
    throw new Error("Failed to create metafield definition");
    }
    }

    // Read existing persisted data about product limits from the associated metafield
    const configuration = JSON.parse(
    api.data.validation?.metafields?.[0]?.value ?? "{}",
    );

    // Query product data needed to render the settings UI
    const products = await getProducts(api.query);

    import {
    extend,
    Text,
    Box,
    FunctionSettings,
    Section,
    NumberField,
    BlockStack,
    Banner,
    InlineStack,
    Image,
    } from "@shopify/ui-extensions/admin";

    const TARGET = "admin.settings.validation.render";

    export default extend(TARGET, async (root, api) => {
    const existingDefinition = await getMetafieldDefinition(api.query);
    if (!existingDefinition) {
    // Create a metafield definition for persistence if no pre-existing definition exists
    const metafieldDefinition = await createMetafieldDefinition(api.query);

    if (!metafieldDefinition) {
    throw new Error("Failed to create metafield definition");
    }
    }

    // Read existing persisted data about product limits from the associated metafield
    const configuration = JSON.parse(
    api.data.validation?.metafields?.[0]?.value ?? "{}",
    );

    // Query product data needed to render the settings UI
    const products = await getProducts(api.query);

    renderValidationSettings(root, configuration, products, api);
    });
    import React, { useState } from "react";
    import {
    reactExtension,
    useApi,
    Text,
    Box,
    FunctionSettings,
    Section,
    NumberField,
    BlockStack,
    Banner,
    InlineStack,
    Image,
    type FunctionSettingsError,
    } from "@shopify/ui-extensions-react/admin";
    import { type ValidationSettingsApi } from "@shopify/ui-extensions/admin";

    const TARGET = "admin.settings.validation.render";

    export default reactExtension(
    TARGET,
    async (api: ValidationSettingsApi<typeof TARGET>) => {
    const existingDefinition = await getMetafieldDefinition(api.query);
    if (!existingDefinition) {
    // Create a metafield definition for persistence if no pre-existing definition exists
    const metafieldDefinition = await createMetafieldDefinition(api.query);

    if (!metafieldDefinition) {
    throw new Error("Failed to create metafield definition");
    }
    }

    // Read existing persisted data about product limits from the associated metafield
    const configuration = JSON.parse(
    api.data.validation?.metafields?.[0]?.value ?? "{}",
    );
    import { type RemoteRoot } from "@remote-ui/core";
    import {
    extend,
    Text,
    Box,
    FunctionSettings,
    Section,
    NumberField,
    BlockStack,
    Banner,
    InlineStack,
    Image,
    type ValidationSettingsApi,
    type FunctionSettingsError,
    } from "@shopify/ui-extensions/admin";

    const TARGET = "admin.settings.validation.render";

    export default extend(
    TARGET,
    async (root: RemoteRoot, api: ValidationSettingsApi<typeof TARGET>) => {
    const existingDefinition = await getMetafieldDefinition(api.query);
    if (!existingDefinition) {
    // Create a metafield definition for persistence if no pre-existing definition exists
    const metafieldDefinition = await createMetafieldDefinition(api.query);

    if (!metafieldDefinition) {
    throw new Error("Failed to create metafield definition");
    }
    }

    // Read existing persisted data about product limits from the associated metafield
    const configuration = JSON.parse(
    api.data.validation?.metafields?.[0]?.value ?? "{}",
    );


To link the admin UI extension to the validation function, configure your validation function's TOML file. You can also configure the app's TOML file with necessary access scopes.

  1. Navigate to the validation function directory:

    Terminal

    cd extensions/cart-checkout-validation
  2. Add the following code to the shopify.extension.toml file associated with the validation function:

    shopify.extension.toml

    [extensions.ui]
    handle = "validation-settings-ui"
  3. Make sure that the shopify.app.toml file in your app root folder has the read_products access scope:

    shopify.app.toml

    [access_scopes]
    scopes = "read_products"
    Note

    If you're adding new access scopes to an existing app, then you need to redeploy and reinstall the app on the store.


Anchor to Step 4: Test the validation on your development storeStep 4: Test the validation on your development store

Run your development server and test the validation function and the corresponding admin UI extension on your development store. You can test the validation behavior directly on checkout, or using the GraphQL Storefront API.

  1. If you're developing a function in a language other than JavaScript or TypeScript, ensure you have configured build.watch in your function extension configuration.
  1. Navigate back to your app root:

    Terminal

    cd ../..
  1. Use the Shopify CLI dev command to start app preview:

    Terminal

    shopify app dev

    You can keep the preview running as you work on your function. When you make changes to a watched file, Shopify CLI rebuilds your function and updates the function extension's drafts, so you can immediately test your changes.

  2. Follow the CLI prompts to preview your app, and install it on your development store.

  1. From the Shopify admin, go to Settings > Checkout.

  2. Under Checkout rules, click Add rule. A new page opens and shows a list of checkout rules.

    Adding a checkout rule via the Checkout settings page in the Shopify admin
  3. Find the cart-checkout-validation function that you want to test and select it.

  4. In the validation configuration, set the limit to five for each product variant.

  5. Click Save, but don't turn on the validation yet.

  1. Before turning on the validation, create a cart that exceeds the quantity limit you set. For example, in your development store, create a cart with a quantity of 10 products.

  2. Go back to the checkout rules page in the Shopify admin and enable this validation by clicking on Turn on.

    Configure a checkout rule via the Checkout settings page in the Shopify admin
  3. Optional. Control how checkout behaves when encountering runtime exceptions by selecting the validation under Checkout rules and toggling Allow all customers to complete checkout.

  4. Complete a checkout in your online store and verify that the validation error message displays.

    A checkout with an error about a product quantity that is too high
  5. Verify that checkout progress is blocked. Clicking the Continue to shipping button in 3-page checkout, or the Pay now button in 1-page checkout, shouldn't redirect the user.

  1. You can also verify through the GraphQL Storefront API. Once the validation is turned on, create a cart with the cartCreate mutation:

    Create a cart

    mutation cartCreate {
    cartCreate(input: {
    lines: []
    }) {
    cart {
    id
    }
    }
    }
  2. Using the Storefront API cartLinesAdd mutation, confirm that the mutation's userErrors field contains the function's error message, and that executing the mutation was unsuccessful.

    Add line items to a cart

    GraphQL mutation

    mutation cartCreate {
    cartCreate(input: {
    lines: []
    }) {
    cart {
    id
    }
    }
    }

    Output

    {
    "data": {
    "cartLinesAdd": {
    "cart": null,
    "userErrors": [
    {
    "code": "VALIDATION_CUSTOM",
    "field": [
    "cartId"
    ],
    "message": "Orders are limited to a maximum of 5 of Monstera"
    }
    ]
    }
    }
    }

Anchor to Debugging using logsDebugging using logs


Anchor to Step 5: Deploy to productionStep 5: Deploy to production

When you're ready to release your changes to users, you can create and release an app version. An app version is a snapshot of your app configuration and all extensions.

  1. Navigate to your app directory.

  2. Run the following command.

    Optionally, you can provide a name or message for the version using the --version and --message flags.

    Terminal

    shopify app deploy

Releasing an app version replaces the current active version that's served to stores that have your app installed. It might take several minutes for app users to be upgraded to the new version.

Tip

If you want to create a version, but avoid releasing it to users, then run the deploy command with a --no-release flag. You can release the unreleased app version using Shopify CLI's release command, or through the Partner Dashboard.



Was this page helpful?