--- title: Build a Discount Function that has network access description: Learn how to build a Discount Function that can make network calls. source_url: html: 'https://shopify.dev/docs/apps/build/discounts/network-access?extension=rust' md: >- https://shopify.dev/docs/apps/build/discounts/network-access.md?extension=rust --- # Build a Discount Function that has network access In this tutorial, you'll build a Shopify Function that validates discount codes against an external system then applies valid discounts to a buyer's cart. Using network access, you'll configure HTTP requests to fetch data from external services and provide that data as input to your run Function. To learn more, see [network access for Shopify Functions](https://shopify.dev/docs/apps/build/functions/input-output/network-access). Shopify for enterprise Network access for [Shopify Functions](https://shopify.dev/docs/apps/build/functions) is limited to [Shopify for enterprise](https://www.shopify.com/enterprise) plans. For more information, contact [our enterprise sales team](https://www.shopify.com/solutions/shop-pay/enterprise#contact-sales). ## What you'll learn In this tutorial, you'll learn how to do the following tasks: * Add a network request to your Function. * Respond to the network request with a mock HTTP server. * Create a tunnel using a service like Ngrok to expose your mock HTTP server to the internet. * Deploy your Function to Shopify. * Review logs for your Function. * Test your Function in checkout. ## Requirements [Create a Partner account](https://www.shopify.com/partners) [Create a development store](https://shopify.dev/docs/apps/tools/development-stores#create-a-development-store-to-test-your-app) [Scaffold an app with a Discount Function](https://shopify.dev/docs/apps/build/discounts/build-discount-function) For example, follow the [Build a Discount Function](https://shopify.dev/docs/apps/build/discounts/build-discount-function) tutorial, or build your own app and Discount Function using the [Discount API](https://shopify.dev/docs/apps/build/discounts). ## Project ![](https://shopify.dev/images/logos/Rust.svg)![](https://shopify.dev/images/logos/Rust-dark.svg) Rust [View on GitHub](https://github.com/Shopify/discounts-reference-app/tree/main/examples/rust/network-access) Using the app you already have, or the one you created by completing the [Build a Discount Function](https://shopify.dev/docs/apps/build/discounts/build-discount-function) tutorial, you can add network access to your Function. ## Update your Function to access the network In this step, you'll update your Function to access the network and use the `fetchResult` in the `run` target's [input query](https://shopify.dev/docs/api/functions/reference/discount/graphql/input). ### Add network access targets To access a network from your Shopify Function, add the `cart.lines.discounts.generate.fetch` and `cart.delivery-options.discounts.generate.fetch` targets to your `shopify.extension.toml` file. The `[[extensions.targeting]]` sections define the Function targets. Each section includes a `target`, `input_query`, and `export` property. The `target` property defines where the Function is applied. There are two network access targets: * `cart.lines.discounts.generate.fetch`: This target runs before the `cart.lines.discounts.generate.run` target. * `cart.delivery-options.discounts.generate.fetch`: This target runs before the `cart.delivery-options.discounts.generate.run` target. The `input_query` property is the path to the Function's input query file. The input query file defines the Function's input parameters. The `export` property specifies the entry point for your Function's discount calculation logic. ## extensions/discount-function/shopify.extension.toml ```toml api_version = "2025-10" [[extensions]] name = "t:name" handle = "discount-function-rs" type = "function" description = "t:description" [[extensions.targeting]] target = "cart.lines.discounts.generate.run" input_query = "src/cart_lines_discounts_generate_run.graphql" export = "cart_lines_discounts_generate_run" [[extensions.targeting]] target = "cart.delivery-options.discounts.generate.run" input_query = "src/cart_delivery_options_discounts_generate_run.graphql" export = "cart_delivery_options_discounts_generate_run" [[extensions.targeting]] target = "cart.lines.discounts.generate.fetch" input_query = "src/cart_lines_discounts_generate_fetch.graphql" export = "cart_lines_discounts_generate_fetch" [[extensions.targeting]] target = "cart.delivery-options.discounts.generate.fetch" input_query = "src/cart_delivery_options_discounts_generate_fetch.graphql" export = "cart_delivery_options_discounts_generate_fetch" [extensions.build] command = "cargo build --target=wasm32-wasip1 --release" path = "target/wasm32-wasip1/release/discount-function-rs.wasm" watch = [ "src/**/*.rs" ] ``` ### Query the `enteredDiscountCodes` for the cart fetch target Create a `cart_lines_discounts_generate_fetch.graphql` [input query](https://shopify.dev/docs/api/functions/reference/discount/graphql/input) file that provides data to your Function. This allows you to conditionally build an HTTP request that is then executed by Shopify. For example, you can query [`enteredDiscountCodes`](https://shopify.dev/docs/api/functions/reference/discount/graphql/input), which allow you to perform validations against your external service. ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_fetch.graphql ```graphql query Input { enteredDiscountCodes { code } cart { buyerIdentity { email } } } ``` ### Query the `enteredDiscountCodes` for the delivery fetch target Create a `cart_delivery_options_discounts_generate_fetch.graphql` [input query](https://shopify.dev/docs/api/functions/reference/discount/graphql/input) file that provides data to your Function. This allows you to conditionally build an HTTP request that is then executed by Shopify. For example, you can query [`enteredDiscountCodes`](https://shopify.dev/docs/api/functions/reference/discount/graphql/input), which allow you to perform validations against your external service. ## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_fetch.graphql ```graphql query Input { enteredDiscountCodes { code } cart { buyerIdentity { email } } } ``` ### Query the `fetchResult` for the cart run target Update the `cart_lines_discounts_generate_run.graphql` file. The `fetchResult` from the fetch request will be the input to your `cart.lines.discounts.generate.run` target. Refer to [Discount API](https://shopify.dev/docs/api/functions/reference/discount) for more information about the input data available to your Function. ## /examples/rust/network-access/extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.graphql ```graphql query Input { fetchResult { jsonBody } cart { buyerIdentity { email } lines { id cost { subtotalAmount { amount } } } } discount { discountClasses } } ``` ### Query the `fetchResult` for the delivery run target Update the `cart_delivery_options_discounts_generate_run.graphql` file. The `fetchResult` from the fetch request will be the input to your `cart.delivery-options.discounts.generate.run` Function. Refer to [Discount API](https://shopify.dev/docs/api/functions/reference/discount) for more information about the input data available to your Function. ## /examples/rust/network-access/extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_run.graphql ```graphql query Input { fetchResult { jsonBody } cart { buyerIdentity { email } deliveryGroups { id } } discount { discountClasses } } ``` ### Add an export for the cart fetch target Shopify uses the `cart.lines.discounts.generate.fetch` target to build an HTTP request, which it executes and provides to your run target as input. ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_fetch.rs ```rust use super::schema; use shopify_function; use shopify_function::prelude::*; use std::collections::BTreeMap; #[shopify_function] fn cart_lines_discounts_generate_fetch( input: schema::cart_lines_discounts_generate_fetch::Input, ) -> shopify_function::Result { let entered_discount_codes = &input.entered_discount_codes(); let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array( entered_discount_codes .iter() .map(|code| JsonValue::String(code.code().to_string())) .collect(), ), )])); let request = schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), json_body: Some(json_body.clone()), body: None, }; Ok(schema::CartLinesDiscountsGenerateFetchResult { request: Some(request), }) } #[cfg(test)] mod tests { use super::*; use serde_json::json; use shopify_function::run_function_with_input; #[test] fn adds_entered_discount_codes_to_json_body_for_cart() -> shopify_function::Result<()> { let input = json!({ "enteredDiscountCodes": [{"code": "SUMMER10"}, {"code": "WELCOME20"}], "cart": { "lines": [] } }) .to_string(); let result = run_function_with_input(cart_lines_discounts_generate_fetch, &input)?; let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array(vec![ JsonValue::String("SUMMER10".to_string()), JsonValue::String("WELCOME20".to_string()), ]), )])); let expected = schema::CartLinesDiscountsGenerateFetchResult { request: Some(schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), json_body: Some(json_body), body: None, }), }; assert_eq!(result, expected); Ok(()) } } ``` ### Add an export for the delivery fetch target Shopify uses the `cart.delivery-options.discounts.generate.fetch` target to build an HTTP request, which it executes and provides to your run target as input. ## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_fetch.rs ```rust use super::schema; use shopify_function; use shopify_function::prelude::*; use std::collections::BTreeMap; #[shopify_function] fn cart_delivery_options_discounts_generate_fetch( input: schema::cart_delivery_options_discounts_generate_fetch::Input, ) -> shopify_function::Result { let entered_discount_codes = &input.entered_discount_codes(); let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array( entered_discount_codes .iter() .map(|code| JsonValue::String(code.code().to_string())) .collect(), ), )])); let request = schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), body: None, json_body: Some(json_body.clone()), }; Ok(schema::CartDeliveryOptionsDiscountsGenerateFetchResult { request: Some(request), }) } #[cfg(test)] mod tests { use super::*; use serde_json::json; use shopify_function::run_function_with_input; #[test] fn adds_entered_discount_codes_to_json_body_for_delivery() -> shopify_function::Result<()> { let input = json!({ "enteredDiscountCodes": [{"code": "FREESHIP"}], "cart": { "lines": [] } }) .to_string(); let result = run_function_with_input(cart_delivery_options_discounts_generate_fetch, &input)?; let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array(vec![JsonValue::String("FREESHIP".to_string())]), )])); let expected = schema::CartDeliveryOptionsDiscountsGenerateFetchResult { request: Some(schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), json_body: Some(json_body), body: None, }), }; assert_eq!(result, expected); Ok(()) } } ``` ### Parse the result from the network call in the cart run target Update the `cart.lines.discounts.generate.run` target to handle the response from the network call. This target parses the `fetchResult` from the network call and uses it to apply a discount to the cart lines. ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.rs ```rust use super::schema; use shopify_function::prelude::*; use shopify_function::Result; #[derive(Deserialize)] #[shopify_function(rename_all = "camelCase")] pub struct OperationItem { product_discounts_add: Option, order_discounts_add: Option, entered_discount_codes_accept: Option, } pub type JsonBody = Vec; #[shopify_function] fn cart_lines_discounts_generate_run( input: schema::cart_lines_discounts_generate_run::Input, ) -> Result { let fetch_result = input.fetch_result().ok_or("Missing fetch result")?; let discount_classes = &input.discount().discount_classes(); // Check if relevant discount classes are set let has_order_discount_class = discount_classes.contains(&schema::DiscountClass::Order); let has_product_discount_class = discount_classes.contains(&schema::DiscountClass::Product); // If no relevant discount class is set, return empty operations if !has_order_discount_class && !has_product_discount_class { return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] }); } // Use jsonBody which is the only available property let operation_items = fetch_result .json_body() .ok_or("Missing json_body in response")?; // Convert the response into operations let mut operations = Vec::new(); ``` ### Parse the result from the network call in the delivery run target Update the `cart.delivery-options.discounts.generate.run` target to handle the response from the network call. This target parses the `fetchResult` from the network call and uses it to apply a discount to the delivery options. ## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_run.rs ```rust use super::schema; use shopify_function::prelude::*; use shopify_function::Result; #[derive(Deserialize)] #[shopify_function(rename_all = "camelCase")] pub struct OperationItem { delivery_discounts_add: Option, entered_discount_codes_accept: Option, } pub type JsonBody = Vec; #[shopify_function] fn cart_delivery_options_discounts_generate_run( input: schema::cart_delivery_options_discounts_generate_run::Input, ) -> Result { let fetch_result = input.fetch_result().ok_or("Missing fetch result")?; let discount_classes = &input.discount().discount_classes(); // Check if shipping discount class is set let has_shipping_discount_class = discount_classes.contains(&schema::DiscountClass::Shipping); // If shipping discount class is not set, return empty operations if !has_shipping_discount_class { return Ok(schema::CartDeliveryOptionsDiscountsGenerateRunResult { operations: vec![] }); } // Use jsonBody which is the only available property let operation_items = fetch_result .json_body() .ok_or("Missing json_body in response")?; // Convert the response into operations let mut operations = Vec::new(); // Process each operation item for item in operation_items { // Always include discount code operations if let Some(validations) = &item.entered_discount_codes_accept { operations.push(schema::DeliveryOperation::EnteredDiscountCodesAccept( validations.clone(), )); } // Include delivery discounts (shipping discount class is already verified) if let Some(delivery_discounts_add_operation) = &item.delivery_discounts_add { operations.push(schema::DeliveryOperation::DeliveryDiscountsAdd( delivery_discounts_add_operation.clone(), )); } // Ignore cart/order discounts for delivery operations } Ok(schema::CartDeliveryOptionsDiscountsGenerateRunResult { operations }) } ``` ### Export the fetch request targets in your main file Add the `cart_lines_discounts_generate_fetch` and `cart_delivery_options_discounts_generate_fetch` exports to the `main.rs` file. The `main.rs` file is the entry point for your Shopify Function. ## extensions/discount-function/src/main.rs ```rust use std::process; pub mod cart_delivery_options_discounts_generate_run; pub mod cart_lines_discounts_generate_run; pub mod cart_delivery_options_discounts_generate_fetch; pub mod cart_lines_discounts_generate_fetch; use shopify_function::typegen; #[typegen("schema.graphql")] pub mod schema { #[query("src/cart_delivery_options_discounts_generate_fetch.graphql")] pub mod cart_delivery_options_discounts_generate_fetch {} #[query("src/cart_lines_discounts_generate_fetch.graphql")] pub mod cart_lines_discounts_generate_fetch {} #[query( "src/cart_delivery_options_discounts_generate_run.graphql", custom_scalar_overrides = { "Input.fetchResult.jsonBody" => super::cart_delivery_options_discounts_generate_run::JsonBody } )] pub mod cart_delivery_options_discounts_generate_run {} #[query ( "src/cart_lines_discounts_generate_run.graphql", custom_scalar_overrides = { "Input.fetchResult.jsonBody" => super::cart_lines_discounts_generate_run::JsonBody } )] pub mod cart_lines_discounts_generate_run {} } fn main() { eprintln!("Please invoke a named export."); process::exit(1); } ``` ### Ensure the name of the function package, matches the handle of your Function In the `Cargo.toml` file, ensure the name of the function package matches the handle of your Function. The handle is the name you provided when you created your Function and it can be found in the `shopify.extension.toml` file. ## extensions/discount-function/Cargo.toml ```toml [package] name = "discount-function-rs" version = "1.0.0" edition = "2021" [dependencies] shopify_function = "1.1.0" [dev-dependencies] serde_json = "1.0" [profile.release] lto = true opt-level = 'z' strip = true ``` ### Re-generate the schema You must regenerate the new Discount API schema to support the new properties you added to your Function Input and Output. ## Terminal shopify app function schema ## Set up a web service to respond to your Function's network requests To provide network access to your Function, set up an external service that can respond to the HTTP requests that Shopify will make on behalf of your Function. When your Shopify Function runs, it cannot make network requests directly. Instead, Shopify makes the requests on behalf of your Function to URLs you specify. In a production environment, this would typically be an API endpoint within your existing infrastructure. For this tutorial, we'll create a simple mock HTTP server. Caution When you test this server in development, there is no URL validation against the value found in the JSON web token. Since the URL of your tunnel can change often, you can ignore validating the URL. In production, you should validate the URL to ensure it matches the value found in the JSON web token. ### Setup a mock HTTP server with Remix Info This is a separate app from your Shopify App. Create it in a new folder outside your Shopify app. 1. Create a new Remix app from the Remix template: In a separate folder from your Shopify app, run the following command to create a new Remix app. This will create a new directory called `mock-http-server` with the Remix app inside it. ## Terminal ```bash npx create-remix@latest mock-http-server --template remix-run/remix/templates/remix-javascript ``` 1. Navigate to the mock HTTP server directory ## Terminal ```bash cd mock-http-server ``` 1. Install the `jsonwebtoken` package: ## Terminal ```bash npm i jsonwebtoken ``` ### Create an API route in the routes directory Create a file named `api.js` in the `/routes` directory of the `mock-http-server` app. The code authenticates the request by verifying the JWT, and then executes the the associated business logic, which returns discount operations. ### Review the response from the mock HTTP server The mock HTTP server responds to your Function's HTTP requests, which are made by Shopify with an object similar to this sample code. This object can then be queried as the `fetchResult` for the input of your `cart.lines.discounts.generate.run` and `cart.delivery-options.discounts.generate.run` targets. ## app/routes/api.js ```javascript import { subtle } from "crypto"; import { TextEncoder } from "util"; import { json } from "@remix-run/node"; import jwt from "jsonwebtoken"; const selectionStrategy = { All: "ALL", First: "FIRST", Maximum: "MAXIMUM", }; const PRODUCT_DISCOUNT_CODE = "10OFFPRODUCT"; const ORDER_DISCOUNT_CODE = "20OFFORDER"; const SHIPPING_DISCOUNT_CODE = "FREESHIPPING"; export const action = async ({ request }) => { if (request.method.toUpperCase() !== "POST") { return json({ error: "Invalid request method. Only POST requests are allowed.", }); } let body; try { body = await authenticate(request); } catch (err) { return json({ error: err.message }); } return handle(body); }; const authenticate = async (request) => { const requestJwtHeader = request.headers.get("x-shopify-request-jwt"); const requestIdHeader = request.headers.get("x-shopify-request-id"); ``` ### Create an environment file in the root of your app This file contains the environment variables for your application: * `APP_CLIENT_SECRET`: Find this on your App's overview page in the Partner Dashboard * `JWT_SHOP_ID`: Find this in the URL of your Store's overview page in the Partner Dashboard ### Run your mock HTTP server locally Run your mock HTTP server locally using the following command: ## Terminal ```bash npm run dev ``` ### Use a service like Ngrok to expose your local server to the internet. To setup Ngrok, follow [these steps](https://shopify.dev/docs/apps/build/cli-for-apps/use-ngrok-tunneling#set-up-ngrok). Then you can create a tunnel to your local server. In a separate terminal, start the Ngrok tunnel: ## Terminal ```bash ngrok http 5173 ``` ### Update the `url` in your cart fetch target In your `cart_lines_discounts_generate_fetch.rs` file, replace the `url` with that of the Ngrok tunnel you created in the previous step. ## Terminal ```bash https:///api ``` ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_fetch.rs ```rust use super::schema; use shopify_function; use shopify_function::prelude::*; use std::collections::BTreeMap; #[shopify_function] fn cart_lines_discounts_generate_fetch( input: schema::cart_lines_discounts_generate_fetch::Input, ) -> shopify_function::Result { let entered_discount_codes = &input.entered_discount_codes(); let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array( entered_discount_codes .iter() .map(|code| JsonValue::String(code.code().to_string())) .collect(), ), )])); let request = schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), json_body: Some(json_body.clone()), body: None, }; Ok(schema::CartLinesDiscountsGenerateFetchResult { request: Some(request), }) } #[cfg(test)] mod tests { use super::*; use serde_json::json; use shopify_function::run_function_with_input; #[test] fn adds_entered_discount_codes_to_json_body_for_cart() -> shopify_function::Result<()> { let input = json!({ "enteredDiscountCodes": [{"code": "SUMMER10"}, {"code": "WELCOME20"}], "cart": { "lines": [] } }) .to_string(); let result = run_function_with_input(cart_lines_discounts_generate_fetch, &input)?; let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array(vec![ JsonValue::String("SUMMER10".to_string()), JsonValue::String("WELCOME20".to_string()), ]), )])); let expected = schema::CartLinesDiscountsGenerateFetchResult { request: Some(schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), json_body: Some(json_body), body: None, }), }; assert_eq!(result, expected); Ok(()) } } ``` ### Update the `url` in your delivery fetch target In your `cart_delivery_options_discounts_generate_fetch.rs` file, replace the `url` with that of the Ngrok tunnel you created in the previous step. ## Terminal ```bash https:///api ``` ## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_fetch.rs ```rust use super::schema; use shopify_function; use shopify_function::prelude::*; use std::collections::BTreeMap; #[shopify_function] fn cart_delivery_options_discounts_generate_fetch( input: schema::cart_delivery_options_discounts_generate_fetch::Input, ) -> shopify_function::Result { let entered_discount_codes = &input.entered_discount_codes(); let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array( entered_discount_codes .iter() .map(|code| JsonValue::String(code.code().to_string())) .collect(), ), )])); let request = schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), body: None, json_body: Some(json_body.clone()), }; Ok(schema::CartDeliveryOptionsDiscountsGenerateFetchResult { request: Some(request), }) } #[cfg(test)] mod tests { use super::*; use serde_json::json; use shopify_function::run_function_with_input; #[test] fn adds_entered_discount_codes_to_json_body_for_delivery() -> shopify_function::Result<()> { let input = json!({ "enteredDiscountCodes": [{"code": "FREESHIP"}], "cart": { "lines": [] } }) .to_string(); let result = run_function_with_input(cart_delivery_options_discounts_generate_fetch, &input)?; let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array(vec![JsonValue::String("FREESHIP".to_string())]), )])); let expected = schema::CartDeliveryOptionsDiscountsGenerateFetchResult { request: Some(schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), json_body: Some(json_body), body: None, }), }; assert_eq!(result, expected); Ok(()) } } ``` ### Add your tunnel URL to the list of allowed hosts in your mock-http-server's Vite config To allow your tunnel to proxy traffic to localhost, add the tunnel URL to the list of approved hosts in your Vite config. This is necessary because Vite uses a proxy to forward requests to your local server. **Don't include the `https://` or `http://` prefix in the URL**. ## vite.config.js ```javascript import { vitePlugin as remix } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ remix({ future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_singleFetch: true, v3_lazyRouteDiscovery: true, }, }), ], server: { allowedHosts: [""], }, }); ``` ### Start your app to preview changes 1. Save your updated configuration TOML file. 2. Start `app dev` if it's not already running: ## Terminal ```bash shopify app dev ``` The configuration TOML file changes will be applied automatically on the development store. ## Test the discount with network access ### Trigger the discount on your development store 1. From your Shopify admin, go to [**Discounts**](https://www.shopify.com/admin/discounts). You should now see the **Cart line, order and shipping** that you created. ![An image showing a list of all active discounts for the store.](https://shopify.dev/assets/assets/apps/discounts/functions-discount-list-multi-class-bMqqn_8b.png) 2. Open your development store and build a cart with a single item in it. 3. Add `10OFFPRODUCT`, `20OFFORDER`, and `FREESHIPPING` codes in the **Discount codes or gift card** field. 4. After you navigate to the checkout page, shipping discounts display on the checkout page after you provide your shipping address. ### Review the Function execution 1. In the terminal where `shopify app dev` is running, review your Function executions. When [testing Functions on development stores](https://shopify.dev/docs/apps/build/functions/test-debug-functions#test-your-function-on-a-development-store), the `dev` output shows Function executions, debug logs you've added, and a link to a local file containing full execution details. 2. In a new terminal window, use the Shopify CLI command [`app function replay`](https://shopify.dev/docs/api/shopify-cli/app/app-function-replay) to [replay a Function execution locally](https://shopify.dev/docs/apps/build/functions/test-debug-functions#execute-the-function-locally-using-shopify-cli). This lets you debug your Function without triggering it again on Shopify. ## Terminal ```terminal shopify app function replay ``` 3. Select the Function execution from the top of the list. Press `q` to quit when you are finished debugging. ## Deploy your app When you're ready to release your changes to users, you can create and release an [app version](https://shopify.dev/docs/apps/launch/deployment/app-versions). 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 ```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`](https://shopify.dev/docs/api/shopify-cli/app/app-release) command, or through the Dev Dashboard. ## extensions/discount-function/shopify.extension.toml ```toml api_version = "2025-10" [[extensions]] name = "t:name" handle = "discount-function-rs" type = "function" description = "t:description" [[extensions.targeting]] target = "cart.lines.discounts.generate.run" input_query = "src/cart_lines_discounts_generate_run.graphql" export = "cart_lines_discounts_generate_run" [[extensions.targeting]] target = "cart.delivery-options.discounts.generate.run" input_query = "src/cart_delivery_options_discounts_generate_run.graphql" export = "cart_delivery_options_discounts_generate_run" [[extensions.targeting]] target = "cart.lines.discounts.generate.fetch" input_query = "src/cart_lines_discounts_generate_fetch.graphql" export = "cart_lines_discounts_generate_fetch" [[extensions.targeting]] target = "cart.delivery-options.discounts.generate.fetch" input_query = "src/cart_delivery_options_discounts_generate_fetch.graphql" export = "cart_delivery_options_discounts_generate_fetch" [extensions.build] command = "cargo build --target=wasm32-wasip1 --release" path = "target/wasm32-wasip1/release/discount-function-rs.wasm" watch = [ "src/**/*.rs" ] ``` ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_fetch.graphql ```graphql query Input { enteredDiscountCodes { code } cart { buyerIdentity { email } } } ``` ## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_fetch.graphql ```graphql query Input { enteredDiscountCodes { code } cart { buyerIdentity { email } } } ``` ## /examples/rust/network-access/extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.graphql ```graphql query Input { fetchResult { jsonBody } cart { buyerIdentity { email } lines { id cost { subtotalAmount { amount } } } } discount { discountClasses } } ``` ## /examples/rust/network-access/extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_run.graphql ```graphql query Input { fetchResult { jsonBody } cart { buyerIdentity { email } deliveryGroups { id } } discount { discountClasses } } ``` ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_fetch.rs ```rust use super::schema; use shopify_function; use shopify_function::prelude::*; use std::collections::BTreeMap; #[shopify_function] fn cart_lines_discounts_generate_fetch( input: schema::cart_lines_discounts_generate_fetch::Input, ) -> shopify_function::Result { let entered_discount_codes = &input.entered_discount_codes(); let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array( entered_discount_codes .iter() .map(|code| JsonValue::String(code.code().to_string())) .collect(), ), )])); let request = schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), json_body: Some(json_body.clone()), body: None, }; Ok(schema::CartLinesDiscountsGenerateFetchResult { request: Some(request), }) } #[cfg(test)] mod tests { use super::*; use serde_json::json; use shopify_function::run_function_with_input; #[test] fn adds_entered_discount_codes_to_json_body_for_cart() -> shopify_function::Result<()> { let input = json!({ "enteredDiscountCodes": [{"code": "SUMMER10"}, {"code": "WELCOME20"}], "cart": { "lines": [] } }) .to_string(); let result = run_function_with_input(cart_lines_discounts_generate_fetch, &input)?; let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array(vec![ JsonValue::String("SUMMER10".to_string()), JsonValue::String("WELCOME20".to_string()), ]), )])); let expected = schema::CartLinesDiscountsGenerateFetchResult { request: Some(schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), json_body: Some(json_body), body: None, }), }; assert_eq!(result, expected); Ok(()) } } ``` ## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_fetch.rs ```rust use super::schema; use shopify_function; use shopify_function::prelude::*; use std::collections::BTreeMap; #[shopify_function] fn cart_delivery_options_discounts_generate_fetch( input: schema::cart_delivery_options_discounts_generate_fetch::Input, ) -> shopify_function::Result { let entered_discount_codes = &input.entered_discount_codes(); let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array( entered_discount_codes .iter() .map(|code| JsonValue::String(code.code().to_string())) .collect(), ), )])); let request = schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), body: None, json_body: Some(json_body.clone()), }; Ok(schema::CartDeliveryOptionsDiscountsGenerateFetchResult { request: Some(request), }) } #[cfg(test)] mod tests { use super::*; use serde_json::json; use shopify_function::run_function_with_input; #[test] fn adds_entered_discount_codes_to_json_body_for_delivery() -> shopify_function::Result<()> { let input = json!({ "enteredDiscountCodes": [{"code": "FREESHIP"}], "cart": { "lines": [] } }) .to_string(); let result = run_function_with_input(cart_delivery_options_discounts_generate_fetch, &input)?; let json_body = JsonValue::Object(BTreeMap::from([( "enteredDiscountCodes".to_string(), JsonValue::Array(vec![JsonValue::String("FREESHIP".to_string())]), )])); let expected = schema::CartDeliveryOptionsDiscountsGenerateFetchResult { request: Some(schema::HttpRequest { headers: vec![ schema::HttpRequestHeader { name: "accept".to_string(), value: "application/json".to_string(), }, schema::HttpRequestHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), }, ], method: schema::HttpRequestMethod::Post, policy: schema::HttpRequestPolicy { read_timeout_ms: 2000, }, url: "/api".to_string(), json_body: Some(json_body), body: None, }), }; assert_eq!(result, expected); Ok(()) } } ``` ## extensions/discount-function/src/cart\_lines\_discounts\_generate\_run.rs ```rust use super::schema; use shopify_function::prelude::*; use shopify_function::Result; #[derive(Deserialize)] #[shopify_function(rename_all = "camelCase")] pub struct OperationItem { product_discounts_add: Option, order_discounts_add: Option, entered_discount_codes_accept: Option, } pub type JsonBody = Vec; #[shopify_function] fn cart_lines_discounts_generate_run( input: schema::cart_lines_discounts_generate_run::Input, ) -> Result { let fetch_result = input.fetch_result().ok_or("Missing fetch result")?; let discount_classes = &input.discount().discount_classes(); // Check if relevant discount classes are set let has_order_discount_class = discount_classes.contains(&schema::DiscountClass::Order); let has_product_discount_class = discount_classes.contains(&schema::DiscountClass::Product); // If no relevant discount class is set, return empty operations if !has_order_discount_class && !has_product_discount_class { return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] }); } // Use jsonBody which is the only available property let operation_items = fetch_result .json_body() .ok_or("Missing json_body in response")?; // Convert the response into operations let mut operations = Vec::new(); ``` ## extensions/discount-function/src/cart\_delivery\_options\_discounts\_generate\_run.rs ```rust use super::schema; use shopify_function::prelude::*; use shopify_function::Result; #[derive(Deserialize)] #[shopify_function(rename_all = "camelCase")] pub struct OperationItem { delivery_discounts_add: Option, entered_discount_codes_accept: Option, } pub type JsonBody = Vec; #[shopify_function] fn cart_delivery_options_discounts_generate_run( input: schema::cart_delivery_options_discounts_generate_run::Input, ) -> Result { let fetch_result = input.fetch_result().ok_or("Missing fetch result")?; let discount_classes = &input.discount().discount_classes(); // Check if shipping discount class is set let has_shipping_discount_class = discount_classes.contains(&schema::DiscountClass::Shipping); // If shipping discount class is not set, return empty operations if !has_shipping_discount_class { return Ok(schema::CartDeliveryOptionsDiscountsGenerateRunResult { operations: vec![] }); } // Use jsonBody which is the only available property let operation_items = fetch_result .json_body() .ok_or("Missing json_body in response")?; // Convert the response into operations let mut operations = Vec::new(); // Process each operation item for item in operation_items { // Always include discount code operations if let Some(validations) = &item.entered_discount_codes_accept { operations.push(schema::DeliveryOperation::EnteredDiscountCodesAccept( validations.clone(), )); } // Include delivery discounts (shipping discount class is already verified) if let Some(delivery_discounts_add_operation) = &item.delivery_discounts_add { operations.push(schema::DeliveryOperation::DeliveryDiscountsAdd( delivery_discounts_add_operation.clone(), )); } // Ignore cart/order discounts for delivery operations } Ok(schema::CartDeliveryOptionsDiscountsGenerateRunResult { operations }) } ``` ## extensions/discount-function/src/main.rs ```rust use std::process; pub mod cart_delivery_options_discounts_generate_run; pub mod cart_lines_discounts_generate_run; pub mod cart_delivery_options_discounts_generate_fetch; pub mod cart_lines_discounts_generate_fetch; use shopify_function::typegen; #[typegen("schema.graphql")] pub mod schema { #[query("src/cart_delivery_options_discounts_generate_fetch.graphql")] pub mod cart_delivery_options_discounts_generate_fetch {} #[query("src/cart_lines_discounts_generate_fetch.graphql")] pub mod cart_lines_discounts_generate_fetch {} #[query( "src/cart_delivery_options_discounts_generate_run.graphql", custom_scalar_overrides = { "Input.fetchResult.jsonBody" => super::cart_delivery_options_discounts_generate_run::JsonBody } )] pub mod cart_delivery_options_discounts_generate_run {} #[query ( "src/cart_lines_discounts_generate_run.graphql", custom_scalar_overrides = { "Input.fetchResult.jsonBody" => super::cart_lines_discounts_generate_run::JsonBody } )] pub mod cart_lines_discounts_generate_run {} } fn main() { eprintln!("Please invoke a named export."); process::exit(1); } ``` ## extensions/discount-function/Cargo.toml ```toml [package] name = "discount-function-rs" version = "1.0.0" edition = "2021" [dependencies] shopify_function = "1.1.0" [dev-dependencies] serde_json = "1.0" [profile.release] lto = true opt-level = 'z' strip = true ``` ## app/routes/api.js ```javascript import { subtle } from "crypto"; import { TextEncoder } from "util"; import { json } from "@remix-run/node"; import jwt from "jsonwebtoken"; const selectionStrategy = { All: "ALL", First: "FIRST", Maximum: "MAXIMUM", }; const PRODUCT_DISCOUNT_CODE = "10OFFPRODUCT"; const ORDER_DISCOUNT_CODE = "20OFFORDER"; const SHIPPING_DISCOUNT_CODE = "FREESHIPPING"; export const action = async ({ request }) => { if (request.method.toUpperCase() !== "POST") { return json({ error: "Invalid request method. Only POST requests are allowed.", }); } let body; try { body = await authenticate(request); } catch (err) { return json({ error: err.message }); } return handle(body); }; const authenticate = async (request) => { const requestJwtHeader = request.headers.get("x-shopify-request-jwt"); const requestIdHeader = request.headers.get("x-shopify-request-id"); ``` ## .env ```bash # Found in the Partner Dashboard APP_CLIENT_SECRET=123456789asdfg # JWT headers for verification JWT_HEADERS=accept,content-type # JWT shop ID JWT_SHOP_ID=123456 ``` ## vite.config.js ```javascript import { vitePlugin as remix } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ remix({ future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_singleFetch: true, v3_lazyRouteDiscovery: true, }, }), ], server: { allowedHosts: [""], }, }); ``` ## Tutorial complete! You've successfully created a Discount Function with network access. You can now use this Function to apply discounts that target cart lines, order subtotals, and shipping rates, while validating discount codes against an external system. *** ### Next Steps [![](https://shopify.dev/images/icons/32/build.png)![](https://shopify.dev/images/icons/32/build-dark.png)](https://shopify.dev/docs/apps/build/discounts/build-ui-extension) [Build a UI extension to configure your discount Function](https://shopify.dev/docs/apps/build/discounts/build-ui-extension) [Add configuration to your discounts experience using metafields and build a user interface for your Function.](https://shopify.dev/docs/apps/build/discounts/build-ui-extension) [![](https://shopify.dev/images/icons/32/gear.png)![](https://shopify.dev/images/icons/32/gear-dark.png)](https://shopify.dev/docs/apps/build/discounts/build-ui-with-react-router) [Build a React Router app to configure your discount Function](https://shopify.dev/docs/apps/build/discounts/build-ui-with-react-router) [Add configuration to your discounts experience using metafields and build a user interface for your Function.](https://shopify.dev/docs/apps/build/discounts/build-ui-with-react-router) [![](https://shopify.dev/images/icons/32/gear.png)![](https://shopify.dev/images/icons/32/gear-dark.png)](https://shopify.dev/docs/apps/build/discounts/ux-for-discounts) [Review the UX guidelines](https://shopify.dev/docs/apps/build/discounts/ux-for-discounts) [Review the UX guidelines to learn how to implement discounts in user interfaces.](https://shopify.dev/docs/apps/build/discounts/ux-for-discounts) [![](https://shopify.dev/images/icons/32/gear.png)![](https://shopify.dev/images/icons/32/gear-dark.png)](https://shopify.dev/docs/apps/build/functions) [Learn more about Shopify Functions](https://shopify.dev/docs/apps/build/functions) [Learn more about how Shopify Functions work and the benefits of using Shopify Functions.](https://shopify.dev/docs/apps/build/functions) [![](https://shopify.dev/images/icons/32/graphql.png)![](https://shopify.dev/images/icons/32/graphql-dark.png)](https://shopify.dev/docs/api/functions) [Consult the Shopify Functions API references](https://shopify.dev/docs/api/functions) [Consult the API references for Shopify Functions](https://shopify.dev/docs/api/functions) [![](https://shopify.dev/images/icons/32/app.png)![](https://shopify.dev/images/icons/32/app-dark.png)](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions) [Learn more about deploying app versions](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions) [Learn more about deploying app versions to Shopify](https://shopify.dev/docs/apps/launch/deployment/deploy-app-versions)