Ir al contenido principal

Sección 12

12 · Shopify Functions

Lógica de backend corriendo dentro de Shopify (Wasm).

En una frase

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.

Qué vas a ejecutar en esta sección

  1. rustup target add wasm32-wasip1 — Añadir el target de Wasm para Rust (una sola vez)
  2. shopify app generate extension --type product_discounts --name volume-discount — Scaffold de la función de descuento (una sola vez)
  3. cargo build --target=wasm32-wasip1 --release — Compilar la Function localmente para verificar que el código es correcto
  4. 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/:

Ejecutar Scaffold de la Function

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:

APITargetPara qué sirve
Discount (línea de producto)cart.lines.discounts.generate.runDescuentos aplicados a líneas individuales del carrito
Discount (orden)order.discounts.generate.runDescuento aplicado al total del pedido
Cart Transformcart.transform.runDividir, combinar, o expandir líneas del carrito
Delivery Customizationcart.delivery-customizations.generate.runOcultar, renombrar, o reordenar métodos de envío
Payment Customizationcart.payment-customizations.generate.runOcultar o reordenar métodos de pago
Checkout Validationcart.checkout.validations.generate.runBloquear 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.toml

input.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 = true

El 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 discountAutomatic o un discountCode via Admin API, referenciando el functionId de la Function desplegada
  • Para Cart Transform: crear un nodo cartTransform via 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

Desde Cloudflare Workers

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 antiguo wasm32-wasi. El CLI configura esto automáticamente en el Cargo.toml generado. Si tienes proyectos legacy con wasm32-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

Brew Atlas · Paso 12

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:

Ejecutar Instalar target Wasm (una sola vez)

rustup target add wasm32-wasip1

Desde ~/proyectos/shopify/brew-atlas/brew-atlas-app/extensions/volume-discount/:

Ejecutar Compilar localmente

cargo build —target=wasm32-wasip1 —release

Desde ~/proyectos/shopify/brew-atlas/brew-atlas-app/:

Ejecutar Deploy

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):

Referencia Preview local con JSON de input · opcional

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):

  1. rustup target add wasm32-wasip1 (una sola vez, desde cualquier directorio).
  2. shopify app generate extension --type product_discounts --name volume-discount desde ~/proyectos/shopify/brew-atlas/brew-atlas-app/ (si aún no generaste el scaffold).
  3. cargo build --target=wasm32-wasip1 --release desde ~/proyectos/shopify/brew-atlas/brew-atlas-app/extensions/volume-discount/ (verificación local).
  4. shopify app deploy desde ~/proyectos/shopify/brew-atlas/brew-atlas-app/ (publicar).

Errores comunes

Cuidado

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.

Cuidado

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.

Cuidado

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.

Nota

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.

Tip

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

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é es schema.graphql, y cómo el macro generate_types! los transforma en tipos de Rust
  • Sabes que el target de compilación correcto en 2026 es wasm32-wasip1 (no wasm32-wasi)
  • Entiendes por qué no se puede usar f64 para montos y sabes usar rust_decimal::Decimal como alternativa
  • Sabes que desplegar la Function no la activa — hay que crear un nodo (discountAutomatic, cartTransform, etc.) en el Admin API referenciando el functionId
  • Puedes usar shopify app function run para 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

Quiz · ¿Lo tenés claro?

5 preguntas · respondé para marcar la sección como completada.

  1. 1. ¿Cuál es el target de compilación correcto para Shopify Functions en Rust a partir de 2025?

  2. 2. ¿Por qué se desaconseja usar f32/f64 para montos dentro de una Function?

  3. 3. Desplegaste una Function de descuento con `shopify app deploy`. ¿Qué pasa si no haces nada más?

  4. 4. ¿Qué limitación del runtime de Functions es correcta?

  5. 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?

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.