← Articles · Shopify

Shopify Functions: A Developer's Guide to Custom Discounts and Validation

4 August 2025 · 4 min read

Shopify Functions are one of the most significant additions to the Shopify developer toolkit in years. They let you write custom backend logic — discount calculations, payment method filtering, delivery customisation, order validation — that runs directly on Shopify's servers, at the speed of native Shopify features.

I've built several production Functions and the experience is genuinely different to anything Magento or other platforms offer. Here's what you need to know.

How they work

A Shopify Function is a small programme compiled to WebAssembly (Wasm) that Shopify executes at specific points in the commerce flow. You write it in Rust, JavaScript, or TypeScript (via Javy), define the input data you need via a GraphQL query, and return a structured output that tells Shopify what to do.

The key constraint: Functions have a strict execution limit of 11ms and a 256KB memory cap. You're not making HTTP calls or querying databases. You receive data, apply logic, return a result. That's it.

This sounds limiting, and it is — intentionally. These constraints are what allow Functions to run at scale without degrading checkout performance.

Discount Functions

This is where most developers start. Shopify provides several discount Function APIs:

Product discounts adjust line item prices. You can build logic like "buy 2 of any product in collection X, get 20% off the cheapest" — the kind of complex promotion that previously required hacking Shopify Scripts or using a third-party app.

Order discounts apply at the cart level. Tiered spend thresholds, bundle pricing, customer-segment-specific offers. The logic runs before checkout renders, so the customer sees the correct price immediately.

Shipping discounts modify delivery rates. Free shipping on orders over a threshold, discounted shipping for loyalty members, that sort of thing.

The input query lets you pull in metafield data, customer tags, cart contents, and more. So your discount logic can reference data stored anywhere in Shopify's data model.

Validation Functions

Cart and checkout validation Functions let you enforce business rules. I've used these for:

  • Preventing checkout when certain product combinations are in the cart (regulatory compliance)
  • Enforcing minimum order quantities for wholesale customers
  • Blocking specific delivery methods based on product attributes
  • Validating custom line item properties before an order is placed

The validation runs server-side, which means customers can't bypass it by manipulating the frontend. That's a significant improvement over JavaScript-only validation. These validation and customisation capabilities are a key part of the broader Checkout Extensibility model.

Payment and delivery customisation

Payment customisation Functions let you hide, reorder, or rename payment methods based on cart contents, customer data, or metafields. A common use case: hiding cash on delivery for high-value orders, or only showing invoice payment for B2B customers identified by a customer tag.

Delivery customisation works similarly — rename shipping methods to match your brand language, hide options that don't apply, or sort them in a specific order.

The development experience

The Shopify CLI handles scaffolding, local development, and deployment. You define your Function within a Shopify app, which means you need an app context even if the Function is the only thing in it.

Testing is straightforward. You define input fixtures (JSON representing the cart state) and assert on the output. The feedback loop is fast because there's no server to run — just compile and test locally.

The Rust path gives you better performance and lower compiled size, but JavaScript is perfectly viable for most use cases and far more accessible to the average developer. I've shipped production Functions in both. Unless you're doing heavy computation, go with JavaScript.

Where it gets tricky

The input data available to Functions is defined by the API version. If you need a piece of data that isn't exposed yet, you're stuck. Shopify is steadily expanding what's available, but gaps remain.

Debugging production issues is harder than local development. You can't attach a debugger to a Function running on Shopify's infrastructure. Logging is limited. When something behaves unexpectedly in production, you're largely reliant on reproducing the scenario locally with matching input data.

Version management matters. Functions are tied to your app's deployment. Updating a Function means deploying a new app version, which affects all merchants using that app. For custom apps serving a single store, this is fine. For public apps, it requires careful release management — something I cover in building a Shopify app for the App Store.

Practical advice

Start with a discount Function. It's the most common use case and the documentation is thorough. Build something simple — a tiered discount based on cart total — and get comfortable with the input/output model before tackling more complex scenarios.

Keep your Functions small and focused. One Function per business rule. Don't try to build a single Function that handles all your discount logic — it becomes impossible to test and debug.

Use metafields to make Functions configurable. Store thresholds, percentages, and product collections as metafield values rather than hardcoding them. This lets merchants adjust behaviour without redeploying code.

Functions are where Shopify is heading for all server-side customisation — I discuss where this is going in the future of Shopify checkout. Investing time in learning them now pays off for years.

Need help with this?

If you're working on something related and could use an extra pair of hands, I'm available for project work. No obligation — just a conversation.

Get in touch →