12 · Shopify Functions
Lógica de backend corriendo dentro de Shopify (Wasm).
Shopify Functions son módulos de WebAssembly que se ejecutan dentro de la infraestructura de Shopify para extender la lógica de negocio del backend — descuentos, validaciones de checkout, transformaciones de carrito, personalización de métodos de envío y pago. El módulo Wasm lo compilas tú (en Rust o JavaScript/TypeScript) y lo despliega Shopify. Cero cold start, cero infraestructura que gestionar, y un contrato de entrada/salida estricto definido por el API de la función.
-
rustup target add wasm32-wasip1— Añadir el target de Wasm para Rust (una sola vez) -
shopify app generate extension --type product_discounts --name volume-discount— Scaffold de la función de descuento (una sola vez) -
cargo build --target=wasm32-wasip1 --release— Compilar la Function localmente para verificar que el código es correcto -
shopify app deploy— Publicar la función al registro de Shopify
Buscá los bloques con badge ✓ Ejecutar en el contenido. Los bloques con 📖 Referencia son ilustrativos — no los ejecutes.
Qué es y por qué existe
Antes de Shopify Functions, extender la lógica de checkout de Shopify requería uno de dos caminos: usar las herramientas nativas del Admin (descuentos por porcentaje, por monto, por código) con sus limitaciones, o pagar por un plan Plus y usar checkout.liquid para personalización total — pero con un sistema de plantillas que Shopify deprecó porque era incompatible con el nuevo Checkout Extensibility.
Shopify Functions cierran esa brecha para todos los planes. Con una Function, puedes escribir cualquier lógica de descuento, validación, o transformación de carrito en código — y esa lógica se ejecuta dentro del proceso de checkout de Shopify, sin que tengas que gestionar un servidor, preocuparte por latencia de red, o pagar por infraestructura separada.
El modelo es el siguiente: tu app despliega un módulo de WebAssembly al registro de Shopify. Cuando un comprador avanza en el checkout, Shopify ejecuta ese módulo dentro de su propia infraestructura, le pasa los datos del carrito como input, y aplica el output (los descuentos, las modificaciones, las validaciones) directamente en el proceso de checkout. La llamada es síncrona desde la perspectiva del checkout — pero el módulo no es tu servidor, es código Wasm corriendo en el runtime de Shopify.
Lenguajes soportados
Rust es el lenguaje recomendado. Produce el Wasm más pequeño (menor overhead de arranque), es el más rápido, y el que mejor encaja con el modelo de memoria controlada que Shopify necesita para garantizar el presupuesto de CPU. Si vienes de JavaScript/TypeScript y no conoces Rust, la curva de entrada inicial es empinada pero el resultado compensa.
JavaScript/TypeScript también está soportado mediante el compilador Javy de Shopify, que convierte JS/TS a Wasm. La developer experience es más familiar, pero el módulo Wasm resultante es más grande y tiene un overhead de startup mayor — sigue siendo viable para la mayoría de las funciones, pero en funciones muy frecuentes (todas las actualizaciones de carrito en checkout) el diferencial de performance importa.
El CLI de Shopify genera el scaffolding correcto para cualquiera de los dos lenguajes:
Desde ~/proyectos/shopify/brew-atlas/brew-atlas-app/:
shopify app generate extension —type product_discounts —name volume-discount
El CLI pregunta el lenguaje: Rust o JavaScript.
APIs de Functions disponibles
Shopify expone varios Function APIs, cada uno con su propio contrato de input/output:
| API | Target | Para qué sirve |
|---|---|---|
| Discount (línea de producto) | cart.lines.discounts.generate.run | Descuentos aplicados a líneas individuales del carrito |
| Discount (orden) | order.discounts.generate.run | Descuento aplicado al total del pedido |
| Cart Transform | cart.transform.run | Dividir, combinar, o expandir líneas del carrito |
| Delivery Customization | cart.delivery-customizations.generate.run | Ocultar, renombrar, o reordenar métodos de envío |
| Payment Customization | cart.payment-customizations.generate.run | Ocultar o reordenar métodos de pago |
| Checkout Validation | cart.checkout.validations.generate.run | Bloquear el checkout con mensajes de error personalizados |
Para Brew Atlas, la Function de descuento por volumen es el caso de uso más natural: si el comprador añade seis o más bolsas de café al carrito, aplica un 10% de descuento sobre cada línea.
Estructura del proyecto (Rust)
El CLI genera esta estructura dentro del directorio extensions/ del app:
// brew-atlas-app/extensions/volume-discount/ (directorio de la función)
extensions/
└── volume-discount/
├── Cargo.toml
├── src/
│ └── lib.rs
├── input.graphql
├── schema.graphql
└── shopify.extension.tomlinput.graphql declara qué datos del carrito necesita tu función. Shopify ejecuta esa query contra el carrito del comprador y pasa el resultado como el input de tu función. Solo declaras lo que necesitas — cuanto menos datos pidas, menor es el overhead.
schema.graphql contiene el esquema de tipos del Function API que estás usando. El CLI lo descarga automáticamente cuando generas la extensión. No lo edites manualmente.
src/lib.rs contiene la lógica de la función en Rust.
shopify.extension.toml declara los metadatos de la extensión y el target que implementa.
El TOML de una Function
# brew-atlas-app/extensions/volume-discount/shopify.extension.toml
api_version = "2026-04"
[[extensions]]
name = "Volume discount"
handle = "volume-discount"
type = "function"
[extensions.build]
command = "cargo build --target=wasm32-wasip1 --release"
path = "target/wasm32-wasip1/release/volume_discount.wasm"
[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "input.graphql"El campo command es lo que el CLI ejecuta cuando hace shopify app build o shopify app deploy. El path resultante es el archivo .wasm que se sube al registro de Shopify. El target cart.lines.discounts.generate.run indica que esta función implementa el API de descuentos por línea de producto.
El input.graphql
La query en input.graphql define qué datos del carrito recibe tu función. Para el descuento por volumen necesitamos la cantidad de cada línea y el costo por unidad:
# brew-atlas-app/extensions/volume-discount/input.graphql
query Input {
cart {
lines {
id
quantity
merchandise {
... on ProductVariant {
id
product {
id
tags
}
}
}
cost {
amountPerQuantity {
amount
currencyCode
}
}
}
}
discountNode {
metafield(namespace: "$app", key: "config") {
value
}
}
}El campo discountNode.metafield es la forma estándar de pasar configuración a una Function desde el Admin: guardas la configuración en un metafield del nodo de descuento, y la Function la lee en cada ejecución. Así puedes hacer la Function configurable sin recompilar el Wasm.
La Function en Rust
// brew-atlas-app/extensions/volume-discount/src/lib.rs
use shopify_function::prelude::*;
use shopify_function::Result;
// El macro generate_types! lee input.graphql y schema.graphql y genera
// los tipos de Rust correspondientes al schema de la función.
// Los nombres exactos de los módulos y structs los genera el macro —
// consulta la doc oficial si los nombres no coinciden con tu versión del schema.
generate_types!(query_path = "input.graphql", schema_path = "schema.graphql");
#[shopify_function]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
let lines = &input.cart.lines;
// Sumar la cantidad total de artículos en el carrito
let total_quantity: i64 = lines.iter().map(|line| line.quantity).sum();
// Si hay menos de 6 unidades en total, no aplicar descuento
if total_quantity < 6 {
return Ok(output::FunctionRunResult { operations: vec![] });
}
// Construir la operación de descuento para cada línea del carrito.
// Nota: los nombres exactos de los campos (DiscountApplicationStrategy,
// CartLineInput, etc.) se generan desde schema.graphql — si el schema
// de tu api_version difiere, ajusta los nombres según el schema generado.
// No asumas que los nombres de este ejemplo son canónicos para todas las versiones.
let operations = lines
.iter()
.map(|line| {
// Target: la línea específica del carrito
let target = output::Target::CartLine(output::CartLineTarget {
id: line.id.clone(),
quantity: None,
});
// Operación de descuento: 10% sobre el precio de la línea
output::Operation::Discount(output::DiscountOperation {
direct_discount: output::DirectDiscount {
value: output::Value::Percentage(output::Percentage {
value: Decimal::from_str("10.0").unwrap(),
}),
message: Some("Caja x6 · 10% off".to_string()),
},
targets: vec![target],
})
})
.collect();
Ok(output::FunctionRunResult { operations })
}El macro generate_types! es lo que hace posible trabajar con tipos fuertemente tipados en Rust sin escribir la serialización JSON manualmente. Lee los dos archivos .graphql y genera los módulos input y output con los tipos correctos para tu API version y tu input query.
El Decimal viene del crate rust_decimal. Los montos monetarios en Shopify Functions NUNCA deben usar f32 o f64 — Shopify rechaza módulos Wasm que usen operaciones de punto flotante en cálculos de dinero. rust_decimal proporciona aritmética de precisión fija que Shopify acepta.
Cargo.toml de la función
# brew-atlas-app/extensions/volume-discount/Cargo.toml
[package]
name = "volume-discount"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
shopify_function = "*"
rust_decimal = { version = "1", features = ["macros"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[profile.release]
opt-level = "s" # optimizar para tamaño del Wasm
lto = trueEl crate-type = ["cdylib"] es obligatorio para que Rust compile a un archivo .wasm que puede exportar funciones a WebAssembly. Sin él, el compilador produce un ejecutable nativo, no un módulo Wasm.
El perfil release con opt-level = "s" y lto = true reduce el tamaño del Wasm resultante, que es relevante porque Shopify tiene un límite de tamaño del módulo. Para funciones simples, el tamaño rara vez es un problema; para funciones con dependencias pesadas, puede serlo.
Activar la Function
Desplegar la Function con shopify app deploy la sube al registro de Shopify, pero no la activa automáticamente. Para activarla, hay que crear el nodo de Admin GraphQL correspondiente:
- Para una Function de descuento: crear un
discountAutomatico undiscountCodevia Admin API, referenciando elfunctionIdde la Function desplegada - Para Cart Transform: crear un nodo
cartTransformvia Admin API - Para Delivery/Payment Customization: crear el nodo correspondiente
- Para Checkout Validation: crear el nodo
checkoutProfile(o el mecanismo que el API indique — consulta la doc para la versión exacta)
La forma más común de gestionar esa activación es desde tu Admin App: el comerciante va a tu app, hace clic en “Activar descuento por volumen”, y tu app llama al Admin GraphQL API para crear el nodo discountAutomatic. El functionId lo recuperas desde el TOML desplegado o desde la API de extensiones.
# Activar un discount automático basado en la Function
# (Fragmento — el schema exacto puede variar; consulta la doc antes de implementar)
mutation DiscountAutomaticAppCreate($discount: DiscountAutomaticAppInput!) {
discountAutomaticAppCreate(automaticAppDiscount: $discount) {
automaticAppDiscount {
discountId
title
status
}
userErrors {
field
message
}
}
}Puentes mentales
Una Shopify Function es a Shopify lo que un Cloudflare Worker es a Cloudflare: código Wasm que se ejecuta en el edge con latencia mínima, sin gestionar infraestructura. La diferencia crítica: los Cloudflare Workers tienen acceso a la red (pueden hacer fetch a APIs externas); las Shopify Functions no. Son funciones puras — reciben un input del estado del carrito, producen un output, y no pueden comunicarse con el mundo exterior. Si necesitas datos externos en tu función (precios de una API externa, configuración de tu servidor), tienes que precomputarlos y guardarlos en metafields — la función los lee desde el input query.
Estado 2026
Lo relevante al 2026:
- Target Wasm: desde 2025, el target correcto de compilación es
wasm32-wasip1, no el antiguowasm32-wasi. El CLI configura esto automáticamente en elCargo.tomlgenerado. Si tienes proyectos legacy conwasm32-wasi, actualiza el target. - Límites de CPU: el presupuesto de CPU por invocación es de pocos milisegundos. Una función que procesa 100 líneas de carrito con lógica simple está bien dentro del límite; una función con algoritmos O(n²) sobre carritos grandes puede excederlo. Shopify rechaza la invocación si se agota el presupuesto, lo que resulta en un error visible para el comprador en checkout.
- Memoria: el límite de memoria del módulo Wasm es de 2 MB. Para funciones Rust simples, esto no es un problema. Para funciones JS compiladas con Javy, el runtime de JS ya ocupa una porción significativa de ese límite.
- Cero red: las Functions no pueden hacer
fetch, abrir sockets, ni leer el filesystem. Son deterministas: el mismo input siempre produce el mismo output.
Proyecto · Brew Atlas
La extensión volume-discount de Brew Atlas aplica un 10% de descuento a todas las líneas del carrito cuando el comprador añade seis o más unidades de café en total. Es el mecanismo de descuento “caja de 6” que ofrece Brew Atlas a compradores frecuentes.
Los archivos están en ~/proyectos/shopify/brew-atlas/brew-atlas-app/extensions/volume-discount/. Para compilar y desplegar:
Este comando es global — puedes correrlo desde cualquier directorio:
rustup target add wasm32-wasip1
Desde ~/proyectos/shopify/brew-atlas/brew-atlas-app/extensions/volume-discount/:
cargo build —target=wasm32-wasip1 —release
Desde ~/proyectos/shopify/brew-atlas/brew-atlas-app/:
shopify app deploy
La Function se activa creando un discountAutomatic via Admin API desde la pantalla de administración de la app (descrita en el PLAN.md de la sección 11) o directamente desde Admin → Descuentos → Crear descuento → Aplicaciones.
Para testear localmente sin desplegar, puedes usar el comando de preview de funciones (opcional — sirve para debugging pero no es parte del flujo obligatorio):
shopify app function run
El CLI te pide un JSON de input (en el formato que devolvería input.graphql) y ejecuta la función localmente, mostrando el output JSON. Es útil para iterar sin pagar el ciclo completo de deploy.
Qué vas a crear/tocar en esta sección (archivos):
~/proyectos/shopify/brew-atlas/brew-atlas-app/extensions/volume-discount/src/lib.rs— la Function en Rust.~/proyectos/shopify/brew-atlas/brew-atlas-app/extensions/volume-discount/input.graphql— qué datos del carrito pide la Function.~/proyectos/shopify/brew-atlas/brew-atlas-app/extensions/volume-discount/Cargo.toml— deps y perfil de release.~/proyectos/shopify/brew-atlas/brew-atlas-app/extensions/volume-discount/shopify.extension.toml— target y comando de build.
Comandos a ejecutar (orden):
rustup target add wasm32-wasip1(una sola vez, desde cualquier directorio).shopify app generate extension --type product_discounts --name volume-discountdesde~/proyectos/shopify/brew-atlas/brew-atlas-app/(si aún no generaste el scaffold).cargo build --target=wasm32-wasip1 --releasedesde~/proyectos/shopify/brew-atlas/brew-atlas-app/extensions/volume-discount/(verificación local).shopify app deploydesde~/proyectos/shopify/brew-atlas/brew-atlas-app/(publicar).
Errores comunes
El target de compilación cambió de wasm32-wasi a wasm32-wasip1 en 2025. Si usas el target antiguo, el CLI de Shopify rechaza el módulo con un error críptico durante el deploy. El CLI moderno lo configura correctamente en el Cargo.toml generado, pero si migras una Function legacy, actualiza el target en el TOML de la extensión y en el comando de compilación.
No uses f32 o f64 para cálculos monetarios en Rust. Las operaciones de punto flotante en módulos Wasm de Shopify causan rechazo en el validador. Usa rust_decimal::Decimal para todos los montos. El macro dec!() de rust_decimal es la forma más cómoda de declarar constantes: dec!(10.0) produce un Decimal con precisión fija, sin riesgo de redondeo de punto flotante.
Las Shopify Functions se invocan en cada cambio del carrito durante el proceso de checkout. Si el comprador añade un artículo, cambia la cantidad, o aplica un código de descuento, tu función se ejecuta. El presupuesto de CPU es bajo por diseño — Shopify necesita que el checkout responda en milisegundos. Mantén la lógica de la función simple y evita bucles costosos sobre colecciones grandes.
Para pasar configuración a una Function sin recompilar el Wasm, usa metafields en el nodo de descuento (o cart transform, delivery customization, etc.). La convención es guardar la configuración como JSON en un metafield con namespace $app y key config. Tu función lo lee desde el campo discountNode.metafield en el input.graphql. Este patrón permite que la app exponga configuración por comerciante sin un nuevo deploy.
El comando shopify app function run ejecuta tu función localmente con un JSON de input que tú proporcionas. Es el equivalente de un test unitario ad-hoc. Antes de desplegar, úsalo con varios inputs de carrito (carrito pequeño, carrito de exactamente 6 unidades, carrito grande) para verificar que los casos límite producen el output correcto. Los errores de Rust y los panics del runtime Wasm son mucho más fáciles de depurar localmente que en producción.
Checklist senior
- Puedes explicar qué es una Shopify Function: qué ejecuta el módulo Wasm, dónde corre, y qué limitaciones tiene (sin red, sin filesystem, presupuesto de CPU)
- Conoces al menos tres Function APIs y puedes elegir el correcto para un caso de negocio dado (descuento vs. validación vs. cart transform)
- Entiendes la estructura del proyecto: qué hace
input.graphql, qué esschema.graphql, y cómo el macrogenerate_types!los transforma en tipos de Rust - Sabes que el target de compilación correcto en 2026 es
wasm32-wasip1(nowasm32-wasi) - Entiendes por qué no se puede usar
f64para montos y sabes usarrust_decimal::Decimalcomo alternativa - Sabes que desplegar la Function no la activa — hay que crear un nodo (
discountAutomatic,cartTransform, etc.) en el Admin API referenciando elfunctionId - Puedes usar
shopify app function runpara testear la función localmente con un JSON de input - Sabes cómo pasar configuración por comerciante a una Function usando metafields en el nodo de descuento
Quiz · ¿Lo tenés claro?
-
1. ¿Cuál es el target de compilación correcto para Shopify Functions en Rust a partir de 2025?
Shopify migró de wasm32-wasi (preview1, deprecado) a wasm32-wasip1. wasm32-wasip2 aún no es soportado; wasm32-unknown-unknown no trae la runtime WASI.
-
2. ¿Por qué se desaconseja usar f32/f64 para montos dentro de una Function?
Los flotantes IEEE-754 no pueden representar exactamente muchos decimales (0.1, 0.2…). Para dinero se usa rust_decimal::Decimal u operaciones en centavos enteros.
-
3. Desplegaste una Function de descuento con `shopify app deploy`. ¿Qué pasa si no haces nada más?
`deploy` solo sube el módulo Wasm y lo registra. Para que se ejecute hay que crear el nodo correspondiente (discountAutomaticAppCreate, cartTransformCreate, etc.) con el functionId vía Admin API.
-
4. ¿Qué limitación del runtime de Functions es correcta?
Las Functions son deterministas y aisladas: sin red, sin filesystem, sin tiempo real. Todo input viene del query GraphQL declarado en input.graphql.
-
5. Necesitas que cada merchant pueda configurar el umbral de descuento (ej. '10% si compra ≥ 3 items') sin redeployar la Function. ¿Cuál es el patrón estándar?
El patrón canónico es metafields en el nodo (discountNode.metafield). input.graphql los pide, la Function los recibe como datos parseados, y el merchant los edita desde el Admin sin tocar código.
Siguiente
Con las Functions cubriendo la lógica de backend en el checkout, el siguiente bloque aborda la UI en ese mismo contexto: las Checkout UI Extensions permiten renderizar componentes React dentro del flujo de checkout sin comprometer la seguridad ni la performance del checkout de Shopify.