smplx.
Arquitectura Shopify

Modelado de datos en Shopify: Metafields, Metaobjects y Custom Data [2026]

Claudio Gerlich··12 min

En la Guia de arquitectura Shopify cubrimos los fundamentos. Hoy analizamos como modelar datos en Shopify -- la base de toda buena arquitectura.

En smplx., hemos experimentado con diferentes enfoques. Metafields? Metaobjects? Custom apps con bases de datos separadas? Cada opcion tiene su lugar. Tomemos juntos la decision correcta.

El modelo de datos de Shopify

Shopify tiene un modelo de datos rigido pero inteligente:

Shop
|-- Products
|   |-- Variants
|   +-- Metafields (desde 2021)
|-- Collections
|   +-- Metafields
|-- Customers
|   +-- Metafields
|-- Orders
|   +-- Metafields
+-- Metaobjects (desde 2024)
    +-- Fields & Relationships

Las entidades estandar

Products son la pieza central:

  • Title, Description, Vendor
  • Published Status
  • Collections (relacion muchos-a-muchos)
  • Variants (relacion uno-a-muchos)

Variants son los SKUs:

  • Price, SKU, Barcode
  • Inventory
  • Options (Size, Color, etc.)

Collections son categorias:

  • Title, Description
  • Products (muchos-a-muchos)

Pero que haces cuando necesitas mas? Ahi es donde entran los metafields.

Metafields: Adjuntar datos personalizados

Que son los Metafields?

Los metafields son pares clave-valor que puedes adjuntar a cualquier entidad estandar.

Ejemplo: Producto con propiedades adicionales

query GetProduct($id: ID!) {
  product(id: $id) {
    id
    title
    metafields(first: 10) {
      edges {
        node {
          id
          key
          value
          type
        }
      }
    }
  }
}

Tipos de metafields

Shopify ofrece tipos predefinidos:

Tipo Ejemplo Uso
single_line_text "Algodon" Material
multi_line_text "Descripcion larga" Info detallada
number_integer "100" Cantidad
number_decimal "19.99" Precio adicional
date "2026-02-19" Fecha de disponibilidad
date_time "2026-02-19T10:30" Marca de tiempo
boolean true/false Flags Si/No
json {...} Estructura arbitraria
list.single_line_text ["rojo", "azul"] Array de texto
list.number_integer [10, 20] Array de numeros
url "https://..." Enlaces
file_reference File ID Acceso a archivos

Ejemplos de metafields

Ejemplo 1: Info de material para producto

mutation CreateProductMetafield($id: ID!, $metafield: MetafieldsSetInput!) {
  metafieldsSet(ownerId: $id, metafields: $metafield) {
    metafields {
      id
      key
      value
    }
  }
}

Input:

{
  "id": "gid://shopify/Product/123",
  "metafield": {
    "key": "material_composition",
    "value": "{\"cotton\": 85, \"polyester\": 15}",
    "type": "json"
  }
}

Ejemplo 2: Alerta de stock bajo

{
  "key": "low_stock_threshold",
  "value": "10",
  "type": "number_integer"
}

Metafields en el theme (Liquid)

<!-- Mostrar material del producto -->
<p>
  Material: {{ product.metafields.custom.material_composition.value }}
</p>

<!-- Con fallback -->
{% if product.metafields.custom.material_composition %}
  <p>{{ product.metafields.custom.material_composition.value }}</p>
{% else %}
  <p>Material no especificado</p>
{% endif %}

<!-- Parsear JSON -->
{% assign material = product.metafields.custom.material_composition.value | parse_json %}
Algodon: {{ material.cotton }}%

Metaobjects: Datos personalizados estructurados

Que son los Metaobjects?

Los metaobjects son modelos de datos completamente personalizados que tu mismo defines. Son como "tablas personalizadas en Shopify."

Metaobjects vs. Metafields

Aspecto Metafields Metaobjects
Adjunto Si (a Product, Order, etc.) No (independiente)
Estructura Pares clave-valor Modelo de datos completo
Relationships Limitados Si, bidireccionales
Complejidad Datos simples Datos complejos
Consultas Por entidad GraphQL como entidades estandar

Ejemplo de Metaobject: Product Bundle

# Definicion de Metaobject (via Admin)
# Tipo: "product_bundle"
# Campos:
# - bundle_name (single_line_text)
# - bundle_description (multi_line_text)
# - bundle_price (number_decimal)
# - products (list.product_reference)
# - discount_percentage (number_integer)

Consulta en GraphQL:

query GetBundle($id: ID!) {
  metaobject(id: $id) {
    id
    handle
    fields {
      key
      value
    }
  }
}

Resultado:

{
  "metaobject": {
    "id": "gid://shopify/Metaobject/abc123",
    "handle": "summer-bundle",
    "fields": [
      {
        "key": "bundle_name",
        "value": "Summer Bundle"
      },
      {
        "key": "bundle_price",
        "value": "99.99"
      },
      {
        "key": "products",
        "value": ["gid://shopify/Product/123", "gid://shopify/Product/456"]
      }
    ]
  }
}

Relationships: Conectar Metaobjects

Novedad en 2024: Los metaobjects pueden referenciarse mutuamente.

Ejemplo: Brand <-> Product

Metaobject "brand":

Campos:
- brand_name (text)
- brand_logo (file_reference)
- brand_description (text)
- featured_products (list.product_reference) <- Relationship!

En Liquid:

{% if product.metafields.custom.brand %}
  {% assign brand = product.metafields.custom.brand.value %}
  <p>Marca: {{ brand.brand_name }}</p>
  <img src="{{ brand.brand_logo.url }}" alt="">
{% endif %}

Relationships bidireccionales

query {
  products(first: 5) {
    edges {
      node {
        id
        title
        # De vuelta a la Brand (si esta definida)
        brand: metafield(namespace: "custom", key: "brand") {
          value
        }
      }
    }
  }

  # Inverso
  metaobjects(type: "brand", first: 5) {
    edges {
      node {
        id
        fields {
          value  # Estos Products me referencian
        }
      }
    }
  }
}

Mundo real: Caso de estudio Bekateq

Bekateq necesitaba un modelo de datos complejo:

  • Mas de 4.400 lineas de codigo
  • 24 plantillas de collection diferentes
  • Atributos personalizados para cada tipo de producto
  • Cero dependencias externas

Su solucion: Metaobjects + Metafields

El modelo de datos

Product
|-- Metafields
|   |-- technical_specs (json)
|   |-- available_colors (list.text)
|   +-- warranty_period (number_integer)
+-- Relationships via Metafield
    +-- Documentation Metaobject
        |-- manual_pdf
        |-- installation_guide
        +-- faq_entries

Brand (Metaobject)
|-- brand_name
|-- brand_logo
|-- products (list.product_reference)
+-- certifications (list.text)

Category (Metaobject)
|-- category_name
|-- parent_category (category_reference)
+-- featured_products (list.product_reference)

Por que funciona: Bekateq no necesitaba 5 custom apps. Necesitaban un modelo de datos limpio.

Metafields o Metaobjects? Matriz de decision

Escenario Solucion
Info adicional de producto (p. ej., material) Metafield
Muchos campos adicionales en producto Metaobject (luego referenciar)
"Tabla" separada con entidad propia Metaobject
Relacion entre productos Metaobject con relationships
Ampliar datos de cliente Metafield en Customer
Datos de logica de negocio compleja DB externa + Custom App

GraphQL para consultas de Custom Data

Consultar Metafields

query GetProductsWithMaterial {
  products(first: 10, query: "has_metafield:custom.material_composition") {
    edges {
      node {
        id
        title
        metafields(namespace: "custom", first: 10) {
          edges {
            node {
              key
              value
              type
            }
          }
        }
      }
    }
  }
}

Metaobjects con filtrado

query GetBundles {
  metaobjects(type: "product_bundle", first: 10) {
    edges {
      node {
        id
        fields {
          key
          value
        }
      }
    }
  }
}

Consultas complejas: Product + Metaobject + Relationships

query {
  product(id: "gid://shopify/Product/123") {
    id
    title
    variants(first: 5) {
      edges {
        node {
          id
          price
          sku
        }
      }
    }
    # Referencia Metafield a Brand
    brand: metafield(namespace: "custom", key: "brand") {
      reference {
        ... on Metaobject {
          id
          fields {
            key
            value
          }
          # Back-reference
          # products (funcionaria si se define bidireccionalmente)
        }
      }
    }
  }
}

Decisiones practicas de arquitectura

Escenario 1: Product Bundles

Opcion A: Metafield en producto

{
  "key": "bundle_products",
  "value": "[\"gid://shopify/Product/1\", \"gid://shopify/Product/2\"]",
  "type": "list.product_reference"
}

Problema: Si el precio del bundle cambia, tienes que actualizar todos los productos vinculados.

Opcion B: Metaobject "Bundle"

Bundle (Metaobject)
|-- bundle_name
|-- bundle_price
|-- bundled_products (list.product_reference)
+-- discount_amount

Ventaja: El bundle existe de forma independiente. Responsabilidad clara.

Nuestra recomendacion: Opcion B (Metaobject)

Escenario 2: Especificaciones tecnicas

Opcion A: Muchos metafields individuales

- power_consumption
- operating_temperature
- noise_level
- warranty_months

Problema: Para cada nueva especificacion necesitas un nuevo metafield. Inflexible.

Opcion B: Un Metafield JSON

{
  "key": "technical_specs",
  "value": "{\"power\": 100, \"temp_min\": -20, \"temp_max\": 50, \"noise\": 65}",
  "type": "json"
}

Ventaja: Flexible. Nuevas especificaciones sin cambios de esquema.

Nuestra recomendacion: Opcion B (JSON Metafield)

Limites y soluciones alternativas

Limite 1: Sin acceso a fuentes de datos externas

Problema: Necesitas datos de Shopify + datos de ERP combinados en GraphQL.

Solucion: Custom app como middleware

// Tu Custom App
app.post('/api/product-with-erp', async (req, res) => {
  const { productId } = req.body;

  // 1. Datos de Shopify
  const shopifyProduct = await shopify.graphql(`
    query { product(id: "${productId}") { ... } }
  `);

  // 2. Datos del ERP
  const erpData = await erp.getProduct(shopifyProduct.sku);

  // 3. Combinar
  res.json({
    ...shopifyProduct,
    erpInventory: erpData.inventory,
    erpCost: erpData.cost
  });
});

Limite 2: Rendimiento con muchos metafields

Problema: Un producto con 50 metafields = lento de cargar

Solucion: Usar metaobjects estrategicamente

// En lugar de:
// Product {
//   metafield_1, metafield_2, ..., metafield_50
// }

// Mejor:
Product {
  technical_specs: Metaobject,  // 30 specs agrupadas
  marketing_data: Metaobject,   // 15 campos de marketing
  inventory_settings: Metaobject // 5 campos de inventario
}

Limite 3: Sin transacciones entre entidades

Problema: Necesitas actualizaciones atomicas entre Product + Metaobject.

Solucion: Custom app con manejo de webhooks

// Cuando llega el webhook "order.create"
// 1. Actualizar Shopify Product Inventory
// 2. Actualizar Custom Metaobject "sales_stats"
// Con logica de reintentos si uno falla

try {
  await updateProductInventory(orderId);
  await updateSalesStats(orderId);
} catch (error) {
  // Reintentar o revertir
  await logError(orderId, error);
}

Mejores practicas

1. Convencion de namespace

<!-- CORRECTO: Namespace para organizacion -->
product.metafields.custom.material_composition
product.metafields.marketing.hero_text
product.metafields.technical.power_rating

<!-- INCORRECTO: Todo en uno -->
product.metafields.custom.everything

2. Establecer tipos correctamente

// CORRECTO
{
  "key": "quantity",
  "value": "100",
  "type": "number_integer"
}

// INCORRECTO
{
  "key": "quantity",
  "value": "\"100\"",  // String en lugar de integer
  "type": "number_integer"
}

3. Relationships en lugar de JSONs complejos

// INCORRECTO: Fuertemente acoplado
{
  "key": "related_products",
  "value": "[{\"id\": \"123\", \"name\": \"Product\"}, ...]",
  "type": "json"
}

// CORRECTO: Acoplamiento debil
{
  "key": "related_products",
  "value": "[\"gid://shopify/Product/123\", ...]",
  "type": "list.product_reference"
}

4. Versionado para cambios

// Cuando cambias la estructura de metafields
// v1: { material: "cotton" }
// v2: { material: "cotton", origin_country: "India" }

// Versionalizar!
product.metafields.custom.material_data_v2
// Clientes antiguos usan v1, nuevos v2

Checklist: Diseno de modelo de datos

  • He utilizado los campos estandar (Products, Collections, etc.)?
  • Son los metafields la opcion correcta para mis datos personalizados?
  • Deberia usar metaobjects en su lugar?
  • Estan definidos los relationships?
  • He elegido los tipos correctamente?
  • Hay limites que estoy excediendo?
  • Necesito datos externos (entonces: Custom App)?
  • Son consistentes las convenciones de namespace?
  • He considerado las implicaciones de rendimiento?

Sobre el autor

Claudio Gerlich es Technical Architect en smplx. y esta especializado en modelado de datos Shopify desde 2020. Con Bekateq resolvimos elegantemente un modelo de datos complejo con metaobjects -- cero dependencias externas, Shopify puro.

El modelado de datos es la base. Buenos modelos = funcionalidades simples. Malos modelos = caos.

smplx. es Shopify Technical Partner (desde 2020) con sede en Munsterland, NRW.