Minimum and maximum quantity per product
A Shopify validation Function that enforces a minimum and maximum order quantity per product, read from two product metafields. Tested JavaScript.
- Function API
- Cart and Checkout Validation
- Target
- cart.validations.generate.run
- Plan
- Any plan in a public app; Shopify Plus in a custom app
- App scope
- write_validations
- API version
- 2026-10
- Last checked
In short
This validation Function enforces a minimum and maximum quantity per product, adding up all variants of the product. The limits come from two integer product metafields that merchants edit in the admin. It uses the cart.validations.generate.run target of the Cart and Checkout Validation API, version 2026-10, and works on any plan when it ships in a public app.
How to use it
- In the admin, create two product metafield definitions of type Integer: custom.min_quantity and custom.max_quantity.
- Create an app with the Shopify CLI and add the extension: shopify app generate extension --template cart_checkout_validation, with JavaScript.
- Replace the generated shopify.extension.toml and the two files in src/ with the files below, then run the fixtures with shopify app function run.
- Deploy with shopify app deploy and turn the validation on with validationCreate.
- Fill in the limits on the products that need them; products without values have no limit.
Code
shopify.extension.tomlapi_version = "2026-10"
[[extensions]]
name = "Minimum and maximum quantity per product"
handle = "min-max-quantity-per-product"
type = "function"
[[extensions.targeting]]
target = "cart.validations.generate.run"
input_query = "src/cart_validations_generate_run.graphql"
export = "cart-validations-generate-run"
[extensions.build]
command = ""
path = "dist/function.wasm"
src/cart_validations_generate_run.graphqlquery CartValidationsGenerateRunInput {
buyerJourney {
step
}
cart {
lines {
quantity
merchandise {
__typename
... on ProductVariant {
product {
id
title
minQuantity: metafield(namespace: "custom", key: "min_quantity") {
value
}
maxQuantity: metafield(namespace: "custom", key: "max_quantity") {
value
}
}
}
}
}
}
}
src/cart_validations_generate_run.js// @ts-check
/**
* @typedef {import("../generated/api").CartValidationsGenerateRunInput} CartValidationsGenerateRunInput
* @typedef {import("../generated/api").CartValidationsGenerateRunResult} CartValidationsGenerateRunResult
*/
/** Errors about the cart as a whole show at the top of checkout and on the cart. */
const CART_TARGET = "$.cart";
/** @param {{value: string} | null | undefined} metafield */
function readLimit(metafield) {
const limit = Number.parseInt(metafield?.value ?? "", 10);
return Number.isInteger(limit) && limit > 0 ? limit : null;
}
/**
* @param {CartValidationsGenerateRunInput} input
* @returns {CartValidationsGenerateRunResult}
*/
export function cartValidationsGenerateRun(input) {
// While the cart is being built, a shopper adding one unit at a time would be
// stopped by the minimum before reaching it; enforce it from checkout on.
const checkMinimum = input.buyerJourney?.step !== "CART_INTERACTION";
// Limits are per product, so add up all variants (sizes, colors) of it.
/** @type {Map<string, {title: string, quantity: number, min: number | null, max: number | null}>} */
const products = new Map();
for (const line of input.cart.lines) {
if (line.merchandise.__typename !== "ProductVariant") continue;
const { product } = line.merchandise;
const entry = products.get(product.id) ?? {
title: product.title,
quantity: 0,
min: readLimit(product.minQuantity),
max: readLimit(product.maxQuantity),
};
entry.quantity += line.quantity;
products.set(product.id, entry);
}
const errors = [];
for (const { title, quantity, min, max } of products.values()) {
if (checkMinimum && min !== null && quantity < min) {
errors.push({ message: `${title}: the minimum order quantity is ${min}.`, target: CART_TARGET });
} else if (max !== null && quantity > max) {
errors.push({ message: `${title}: you can buy at most ${max}.`, target: CART_TARGET });
}
}
return { operations: errors.length > 0 ? [{ validationAdd: { errors } }] : [] };
}
Test input and expected output
tests/above-maximum.input.json{
"cart": {
"lines": [
{
"quantity": 3,
"merchandise": {
"__typename": "ProductVariant",
"product": { "id": "gid://shopify/Product/1234567892", "title": "Limited Sneaker", "minQuantity": null, "maxQuantity": { "value": "2" } }
}
}
]
}
}
tests/above-maximum.output.json{
"operations": [
{ "validationAdd": { "errors": [{ "message": "Limited Sneaker: you can buy at most 2.", "target": "$.cart" }] } }
]
}
tests/below-minimum-across-variants.input.json{
"cart": {
"lines": [
{
"quantity": 1,
"merchandise": {
"__typename": "ProductVariant",
"product": { "id": "gid://shopify/Product/1234567890", "title": "Linen Napkin", "minQuantity": { "value": "4" }, "maxQuantity": null }
}
},
{
"quantity": 2,
"merchandise": {
"__typename": "ProductVariant",
"product": { "id": "gid://shopify/Product/1234567890", "title": "Linen Napkin", "minQuantity": { "value": "4" }, "maxQuantity": null }
}
}
]
}
}
tests/below-minimum-across-variants.output.json{
"operations": [
{ "validationAdd": { "errors": [{ "message": "Linen Napkin: the minimum order quantity is 4.", "target": "$.cart" }] } }
]
}
tests/below-minimum-while-building-cart.input.json{
"buyerJourney": {
"step": "CART_INTERACTION"
},
"cart": {
"lines": [
{
"quantity": 1,
"merchandise": {
"__typename": "ProductVariant",
"product": {
"id": "gid://shopify/Product/1234567890",
"title": "Linen Napkin",
"minQuantity": {
"value": "4"
},
"maxQuantity": null
}
}
},
{
"quantity": 2,
"merchandise": {
"__typename": "ProductVariant",
"product": {
"id": "gid://shopify/Product/1234567890",
"title": "Linen Napkin",
"minQuantity": {
"value": "4"
},
"maxQuantity": null
}
}
}
]
}
}tests/below-minimum-while-building-cart.output.json{
"operations": []
}tests/within-limits.input.json{
"cart": {
"lines": [
{
"quantity": 4,
"merchandise": {
"__typename": "ProductVariant",
"product": {
"id": "gid://shopify/Product/1234567890",
"title": "Linen Napkin",
"minQuantity": { "value": "4" },
"maxQuantity": { "value": "24" }
}
}
},
{
"quantity": 1,
"merchandise": {
"__typename": "ProductVariant",
"product": { "id": "gid://shopify/Product/1234567891", "title": "Gift Card Sleeve", "minQuantity": null, "maxQuantity": null }
}
}
]
}
}
tests/within-limits.output.json{ "operations": [] }
Written by Lintel. MIT license: free to use in your own store and in client work.
Limits and edge cases
- Quantities are added up per product, so 2 small and 2 large count as 4.
- Errors show on the cart page and at checkout. The Storefront API exposes them on the Cart object too.
- Messages are in English in the code; read localization.language in the input query if you need them in several languages.
- Each metafield read costs 3 of the 30 query cost points a Function input can use; two per product is fine.
Questions
- How do I set a minimum order quantity for a product in Shopify?
- Use a validation Function that reads the minimum from a product metafield and blocks checkout below it, as this example does. In a public app it works on any plan.
- Can I limit how many of a product one customer buys?
- Per order, yes: set custom.max_quantity. Limits across several orders would need the customer’s past orders per product, which the Function input doesn’t include.
- Does the theme’s quantity selector respect the limits?
- No. The Function stops adding more than the maximum to the cart and checks the minimum from checkout on, so shoppers can build up to it. Read the same metafields in the theme to set min and max on the quantity input for a smoother experience.
Shopify documentation
- Written for Function API version 2026-10 and checked against Shopify’s reference on the date shown. Shopify releases a new version every quarter.
- Custom apps that contain Functions only run on Shopify Plus stores; a public app with the same Function runs on any plan unless the page says otherwise.
- Every example ships with test fixtures, and we run them before publishing. Run them again after you change the code: shopify app function run.
- MIT license. Use the code in your own store or in client work.
Need this built, tested and maintained for your store? See Shopify Functions development.
