Construir un feed de producto headless con Node.js: servidor de datos PIM para múltiples canales

Cómo construir una capa de distribución de datos de producto que sirva a diferentes canales con representaciones distintas del mismo dato. Proyecto técnico completo.

Cuando varios canales consumen información del mismo PIM, existen dos enfoques habituales: integrar cada destino directamente contra la plataforma o introducir una capa headless de distribución que lea el dato gobernado en el PIM y entregue a cada consumidor la representación que necesita.

La segunda opción empieza a aportar valor cuando aparecen varios eCommerce, marketplaces, portales B2B, aplicaciones o servicios internos que necesitan versiones diferentes del mismo producto.

La arquitectura conceptual puede resumirse así:

PIM
 ↓
capa headless Node.js
 ↓
eCommerce / marketplace / B2B / app / CMS

El punto más importante es entender qué función cumple esa capa. No reemplaza al PIM ni crea un segundo maestro de producto. El PIM continúa gobernando la información; el servidor headless gobierna cómo esa información se expone, transforma, versiona y distribuye.

En una nota anterior trabajé cómo generar múltiples representaciones desde un mismo maestro. En ésta quiero avanzar un nivel: construir el servicio que realmente puede servir esas representaciones a distintos consumidores.

Un servidor headless de producto no crea otro maestro. Funciona como una capa de distribución entre el dato gobernado por el PIM y las distintas formas en que cada canal necesita consumirlo.

¿Qué es un feed de producto headless?

Un feed de producto headless es una capa desacoplada que toma información de producto desde una fuente gobernada —normalmente un PIM— y la entrega mediante contratos diseñados específicamente para distintos consumidores.

Puede exponer una API REST, generar feeds, ofrecer exportaciones masivas o combinar varios mecanismos.

La diferencia frente a una integración punto a punto es arquitectónica.

En un modelo directo tendríamos:

PIM → eCommerce
PIM → marketplace
PIM → portal B2B
PIM → app

Cada consumidor tiene que entender al PIM.

Con una capa headless:

              ┌→ eCommerce
PIM → Node.js ├→ marketplace
              ├→ B2B
              └→ app

Los consumidores entienden nuestro contrato, mientras la capa intermedia se ocupa de entender al PIM.

Ese desacoplamiento es el principal valor del patrón.

¿Por qué no dejar que cada canal consulte directamente el PIM?

Para un ecosistema pequeño, hacerlo puede ser perfectamente razonable.

Si tenemos un solo eCommerce y la API del PIM satisface correctamente sus necesidades, sumar otro servicio puede ser complejidad innecesaria.

El problema aparece cuando varios consumidores empiezan a repetir la misma lógica: autenticarse, paginar productos, interpretar variantes, resolver locales, reconstruir categorías, seleccionar assets, transformar atributos y administrar errores.

El conocimiento sobre el PIM termina distribuido en cada integración.

Entonces una modificación del modelo puede afectar simultáneamente al storefront, al marketplace, al portal B2B y a cualquier otro consumidor.

Una capa headless introduce una frontera:

consumidor → contrato estable → adapter → PIM

El consumidor deja de necesitar saber si detrás utilizamos Sales Layer, Akeneo, Plytix u otra plataforma. Solamente necesita saber qué contrato de producto expone nuestro servicio.

El objetivo no es esconder el PIM por esconderlo. Es evitar que cada consumidor tenga que aprender y reproducir el funcionamiento interno del maestro de producto.

¿Por qué conviene separar el producto del PIM de nuestro modelo canónico?

Yo evitaría que los endpoints públicos devuelvan directamente la respuesta cruda del proveedor.

Prefiero incorporar una capa intermedia:

modelo del PIM
      ↓
    adapter
      ↓
modelo canónico
      ↓
representación por canal

El modelo canónico representa nuestro dominio de producto.

Por ejemplo:

{
  id: "SKU-1004",
  parentId: "MODEL-100",
  name: {
    es_AR: "Producto ejemplo"
  },
  brand: "Marca X",
  attributes: {},
  categories: [],
  assets: [],
  variants: []
}

Ese objeto no necesita coincidir exactamente con cómo Sales Layer, Akeneo o Plytix representan un producto.

Eso es justamente lo que buscamos.

La capa superior debería depender del concepto de producto de nuestro negocio y no de los nombres de campos utilizados por el vendor.

Si mañana cambia el PIM, el adapter absorbe buena parte de esa diferencia y los consumidores pueden continuar usando el mismo contrato.

¿Cómo organizar un servidor headless de producto en Node.js?

Una estructura posible sería:

src/
  pim/
    client.js
    adapter.js

  domain/
    product.js

  representations/
    ecommerce.js
    marketplace.js
    b2b.js

  serializers/
    json.js
    ndjson.js
    csv.js

  cache/
    repository.js

  api/
    routes.js

  sync/
    changelog.js

  observability/
    logger.js

No hay nada obligatorio en estos nombres. La decisión importante es otra: conectar, modelar, representar, serializar y servir deberían ser responsabilidades separadas.

De esa forma evitamos terminar con un endpoint enorme que autentica contra el PIM, consulta productos, transforma campos, decide el formato de salida, administra el cache y registra errores dentro de una misma función.

Separar esas responsabilidades hace que el servicio pueda crecer sin convertir cada nuevo canal en una modificación riesgosa.

¿Qué función cumple el adapter del PIM?

El adapter aísla la plataforma de origen.

En lugar de repartir esto por toda la aplicación:

const response = await pimApi.get("/products/...");

quiero que el resto del sistema trabaje con algo parecido a:

const product =
  await pimRepository.getProduct(productId);

Internamente, el adapter traduce la respuesta concreta del PIM:

async function getProduct(id) {
  const raw = await pimClient.getProduct(id);

  return {
    id: raw.reference,
    parentId: raw.parent_reference ?? null,
    name: normalizeLocales(raw.name),
    attributes: mapAttributes(raw),
    categories: mapCategories(raw),
    assets: mapAssets(raw),
    variants: mapVariants(raw)
  };
}

La ventaja no es solamente estética.

Si mañana cambia la autenticación, el endpoint, la estructura del producto o incluso la plataforma PIM, tenemos un lugar claramente identificado donde absorber la modificación.

El adapter protege al dominio de las particularidades del proveedor.

¿Cómo generar varias representaciones desde un mismo producto?

Después del modelo canónico aparece la capa de representación.

Un eCommerce podría requerir:

function toEcommerce(product) {
  return {
    sku: product.id,
    title: product.name.es_AR,
    images: selectWebImages(product.assets),
    categories: product.categories,
    attributes: selectCommerceAttributes(product)
  };
}

Un portal B2B puede necesitar algo diferente:

function toB2B(product) {
  return {
    sku: product.id,
    technicalName: product.name.es_AR,
    specifications:
      selectTechnicalAttributes(product),
    documents:
      selectTechnicalDocuments(product.assets)
  };
}

Y un marketplace podría utilizar una tercera transformación.

El producto maestro sigue siendo uno. Lo que cambia es la proyección del producto hacia un contexto de consumo.

Esto es importante porque evita resolver las diferencias de canal duplicando campos dentro del PIM.

El PIM mantiene el dato gobernado. Las representaciones determinan cómo se activa.

¿Qué endpoints debería exponer el feed server?

Me gusta que el canal aparezca explícitamente dentro del contrato.

Por ejemplo:

GET /v1/channels/ecommerce/products/SKU-1004
GET /v1/channels/b2b/products/SKU-1004
GET /v1/channels/marketplace/products/SKU-1004

Para colecciones:

GET /v1/channels/ecommerce/products

Y para consumos incrementales:

GET /v1/channels/ecommerce/products?updated_since=...

Esto deja claro que estamos solicitando una representación específica de un producto, no el producto maestro sin contexto.

También simplifica permisos, cache, versionado, métricas y testing.

Un test del endpoint B2B puede verificar requisitos B2B sin conocer las reglas del marketplace.

La taxonomía también necesita una representación por canal

El problema no termina en productos y variantes.

Una taxonomía interna puede necesitar convertirse en distintas estructuras según el consumidor.

Por ejemplo:

  • árbol de navegación para eCommerce;
  • clasificación técnica para un portal B2B;
  • códigos externos para marketplace;
  • estructura optimizada para un buscador.

No haría que cada aplicación tenga que reconstruir la jerarquía desde una colección de categorías crudas.

Podríamos exponer contratos como:

GET /v1/channels/ecommerce/categories/tree
GET /v1/channels/marketplace/category-mappings
GET /v1/channels/b2b/categories

La capa de distribución vuelve a cumplir el mismo papel: traducir una estructura gobernada a la representación útil para cada consumidor.

Una API de distribución no debería exponer la estructura más cómoda para el sistema origen. Debería exponer la estructura más útil y estable para quien consume el dato.

¿Un servidor headless tiene que devolver siempre JSON?

No.

Headless no significa REST y tampoco significa exclusivamente JSON.

Un frontend o un microservicio probablemente prefieran JSON, pero los ecosistemas de producto suelen tener consumidores muy diferentes.

Una misma capa podría producir:

JSON
NDJSON
CSV
XML

La clave es separar dos decisiones:

qué información representa el producto para ese canal
y
en qué formato técnico la entregamos.

Primero:

const representation =
  toEcommerce(product);

Después:

serializer.write(representation);

Así, decidir que un consumidor quiere NDJSON en lugar de JSON no obliga a modificar las reglas comerciales de la representación.

Representación y serialización son problemas distintos.

¿Conviene consultar el PIM en cada request?

Depende del volumen, la latencia esperada y el papel que queremos darle al PIM en tiempo de ejecución.

La arquitectura más sencilla sería:

cliente
  ↓
feed server
  ↓
PIM
  ↓
feed server
  ↓
cliente

Podemos considerarla un modo passthrough.

Tiene pocas piezas y puede ser suficiente con volúmenes moderados.

Pero cuando aumentan consumidores y requests aparece otra pregunta: ¿queremos que la disponibilidad y la latencia del PIM condicionen cada lectura de producto?

Una alternativa es introducir cache:

PIM → servidor → cache → consumidor

Otra es mantener un read model optimizado para consulta:

PIM
 ↓
sincronización incremental
 ↓
read model
 ↓
API headless
 ↓
consumidores

En ese escenario, la sincronización mantiene actualizado el modelo de lectura y los requests de los consumidores no tienen que consultar permanentemente la fuente original.

Si cada request del storefront obliga a consultar el PIM, el PIM pasa a formar parte del runtime crítico de la experiencia digital. Eso puede ser correcto, pero debería ser una decisión consciente.

La sincronización incremental permite actualizar solamente los productos afectados.

Conceptualmente:

leer último checkpoint
        ↓
consultar cambios
        ↓
recuperar productos afectados
        ↓
actualizar read model
        ↓
invalidar representaciones
        ↓
confirmar nuevo checkpoint

Lo importante es que el checkpoint represente un estado confirmado, no solamente un momento que ya consultamos.

Si recuperamos 500 modificaciones y actualizamos el checkpoint antes de procesarlas, una falla a mitad de camino puede provocar que algunos cambios no vuelvan a recuperarse.

El orden debería ser:

leer → procesar → validar → confirmar

No:

leer → confirmar → esperar que todo salga bien

Esta diferencia es pequeña en código y enorme en confiabilidad.

¿Cómo servir catálogos grandes sin cargar todo en memoria?

Un producto individual no representa demasiado riesgo.

El problema aparece cuando alguien solicita una colección grande:

GET /v1/channels/b2b/products

y tenemos que procesar un catálogo completo.

Evitaría una estrategia como:

const products = await getAllProducts();

return JSON.stringify(products);

cuando el volumen puede crecer significativamente.

En su lugar podemos paginar, procesar por lotes o utilizar streams.

Por ejemplo:

for await (const product of productStream) {
  const output = toB2B(product);

  response.write(
    JSON.stringify(output) + "\n"
  );
}

Si utilizamos streaming, además tenemos que respetar el backpressure: el servidor no debería seguir escribiendo indefinidamente si el consumidor recibe más lento de lo que producimos.

Una versión conceptual sería:

if (!response.write(chunk)) {
  await once(response, "drain");
}

Esto permite que el flujo de datos se adapte a la capacidad real de procesamiento y evita que el buffering crezca sin control.

Cachear también significa saber cuándo invalidar

Agregar un cache es fácil.

Mantenerlo correcto es bastante más interesante.

Supongamos que cambia:

SKU-1004

Podríamos tener almacenado:

ecommerce:SKU-1004
b2b:SKU-1004
marketplace:SKU-1004

Un cambio en el nombre del producto quizá invalide las tres representaciones.

Un cambio en un atributo exclusivamente B2B puede afectar solamente una.

Una modificación de categoría puede involucrar al producto y también a una colección completa o árbol de navegación.

Por eso cache e invalidación deberían diseñarse juntos.

Un TTL puede funcionar como red de seguridad. Los eventos o procesos incrementales permiten invalidar con mayor precisión.

Cachear una representación es sencillo. Saber exactamente cuándo dejó de ser válida es el verdadero problema de diseño.

¿Qué significa validar correctamente una respuesta?

No alcanza con validar que el producto existe en el PIM.

Tenemos dos contratos:

PIM → modelo canónico

y después:

modelo canónico → representación de canal

Un producto puede cumplir perfectamente el primero y fallar en el segundo.

Por ejemplo, la representación eCommerce puede exigir:

  • nombre localizado;
  • imagen principal;
  • categoría publicable.

La salida B2B puede exigir determinadas especificaciones técnicas.

Por eso un 200 OK debería significar algo más fuerte que “encontré el SKU”.

Idealmente significa:

“Pude construir una representación válida para el contrato solicitado.”

¿Cómo manejar datos que no deberían salir del PIM?

Una ventaja de introducir esta capa es poder definir fronteras de acceso.

No todos los consumidores deberían poder leer todos los atributos disponibles en el PIM.

Podemos definir permisos como:

ecommerce-read
marketplace-export
b2b-private
internal-admin

Una aplicación pública no necesita necesariamente campos internos de workflow, notas operativas o información utilizada exclusivamente durante el enriquecimiento.

La representación funciona entonces como una allowlist: expone deliberadamente lo necesario en lugar de devolver todo y confiar en que el consumidor ignore lo que no corresponde.

Eso mejora tanto seguridad como claridad del contrato.

¿Por qué conviene versionar la API desde el principio?

Cuando varios consumidores dependen de un endpoint, cambiar el nombre o la estructura de un campo deja de ser un simple refactor interno.

Ya existe un contrato.

Por eso utilizaría desde el comienzo:

/v1/channels/ecommerce/products

Si algún día necesitamos introducir un cambio incompatible:

/v2/channels/ecommerce/products

podemos mantener temporalmente ambos contratos mientras migran los consumidores.

La separación entre adapter y contrato vuelve a ser importante.

Si cambia internamente el PIM pero podemos absorber el cambio dentro del adapter, no debería ser necesario romper la API pública.

En cambio, si cambia la estructura que ofrecemos al consumidor, estamos frente a una evolución del contrato.

¿Qué datos deberían quedar fuera de esta capa?

Construir un servidor de producto no significa que tengamos que convertirlo en el origen de cualquier información comercial.

Hay que respetar el ownership.

El PIM suele gobernar información relativamente estable y enriquecida, como:

  • descripciones;
  • atributos;
  • taxonomía;
  • relaciones;
  • variantes;
  • assets;
  • contenido por canal.

Datos transaccionales como stock o determinados precios pueden tener otra fuente de autoridad y otra frecuencia de actualización.

Podríamos agregarlos mediante composición si la arquitectura realmente lo necesita, pero no asumiría que el feed de producto tiene que convertirse automáticamente en maestro de datos transaccionales.

Ese límite es importante porque el contenido editorial y los datos transaccionales tienen ritmos y responsabilidades diferentes.

¿Qué observabilidad necesita un servidor de producto?

Esta capa también puede convertirse en un excelente punto de observación del catálogo.

Para cada request registraría datos como:

requestId
channel
productId
representationVersion
cacheHit
sourceVersion
duration
status

A partir de ahí podemos medir:

  • requests por consumidor;
  • latencia;
  • porcentaje de cache hit;
  • errores de representación;
  • productos sin mapping;
  • atraso de sincronización;
  • tamaño de payload;
  • tiempo de procesamiento.

Eso permite responder preguntas que son difíciles de contestar cuando cada sistema consulta al PIM por separado.

Por ejemplo: ¿qué canal consume más catálogo?, ¿dónde aparecen más errores?, ¿cuánto demora un cambio del PIM en estar disponible?, ¿qué representaciones fallan con mayor frecuencia?

La observabilidad deja de ser solamente infraestructura. Empieza a describir cómo circula la información de producto dentro del ecosistema.

¿Cómo se ve este problema en un ecosistema multicanal real?

En proyectos PIM aparecen escenarios donde un mismo maestro termina alimentando ERP, eCommerce, marketplace, POS y otros canales mediante diferentes integraciones.

En uno de los proyectos documentados por CRITERIA, por ejemplo, el PIM participa en un ecosistema que incluye SAP, VTEX, un circuito de publicación hacia Mercado Libre, Retail Pro, GS1 y generación de catálogos. Distintas integraciones Node.js y un orquestador intervienen en esos recorridos.

No estoy diciendo que allí se haya implementado exactamente el servidor headless que describo en este artículo. El caso sirve para mostrar el problema arquitectónico que lo justifica: cuando muchos consumidores necesitan proyecciones distintas del mismo maestro, repetir la lógica de transformación en cada integración empieza a ser costoso y difícil de gobernar.

Una capa común puede resultar atractiva justamente cuando esa duplicación empieza a aparecer.

¿Cuándo no construiría esta capa?

No todo proyecto necesita un feed server headless.

Si tenemos:

PIM → un único eCommerce

y un conector existente resuelve el recorrido correctamente, agregar una API propia probablemente incremente el costo sin resolver un problema real.

Esta arquitectura empieza a tener más sentido cuando aparecen varias de estas condiciones:

  • múltiples consumidores;
  • representaciones diferentes del mismo producto;
  • lógica repetida entre integradores;
  • necesidad de aislar los consumidores del proveedor PIM;
  • grandes volúmenes de lectura;
  • necesidad de cache o read models;
  • contratos de producto que deben mantenerse estables;
  • varios formatos de entrega.

Una capa headless aporta valor cuando centraliza complejidad real. Si solamente agrega otro salto HTTP a una integración sencilla, estamos construyendo arquitectura sin un problema que la justifique.

¿Qué debería revisar antes de llevarlo a producción?

Antes del go-live, revisaría:

  • El PIM continúa siendo claramente la fuente autorizada para los datos que gobierna.
  • Existe un modelo canónico separado del modelo del proveedor.
  • La comunicación específica con el PIM está encapsulada en adapters.
  • Cada consumidor tiene una representación explícita.
  • Los contratos se validan antes de responder.
  • Productos padre y variantes tienen reglas definidas.
  • Taxonomías y mappings están contemplados.
  • Las colecciones utilizan paginación o streaming cuando corresponde.
  • Los grandes feeds no cargan innecesariamente todo el catálogo en memoria.
  • La sincronización incremental utiliza checkpoints seguros.
  • Cache e invalidación fueron diseñados como un mismo problema.
  • Los permisos limitan la información visible para cada consumidor.
  • El contrato público está versionado.
  • Los logs permiten reconstruir una solicitud.
  • Existe observabilidad sobre sincronización, latencia, cache y errores.
  • Está definido qué información transaccional pertenece a otros sistemas.

Y antes de todo eso hay una pregunta todavía más importante:

¿sabemos qué sistema tiene autoridad sobre cada dato?

Si esa definición no existe, la capa headless no va a solucionarla. Solamente va a ocultarla detrás de otra API.

Para mí, el valor de un servidor headless de producto aparece cuando deja de ser un simple proxy del PIM y empieza a funcionar como un contrato estable para todo el ecosistema. El PIM conserva la autoridad sobre el dato; Node.js absorbe adaptación, representaciones, formatos, cache, versionado y distribución; y cada consumidor recibe solamente lo que necesita. Cuando esa separación está bien diseñada, agregar un nuevo canal deja de significar volver a interpretar el PIM desde cero y pasa a significar algo mucho más controlable: construir una nueva proyección sobre un maestro que sigue siendo único.

Foto del avatar

Desarrollador Node.js Senior en CRITERIA Smart Cataloging. Responsable de las integraciones API REST entre plataformas PIM y sistemas de eCommerce, ERP, marketplaces y puntos de venta. Construye los puentes técnicos que conectan el dato de producto con cada canal de distribución.