466 productos, 557 lotes, 0 errores: sincronizamos un ERP con una app web en tiempo real
Un cliente nos dijo: 'Tenemos todo en Holded pero necesitamos un sistema propio de inventario'. Spoiler: la API de Holded no funciona como dice su documentación. Esto es lo que aprendimos.
Un cliente nos contactó con una necesidad concreta:
"Llevamos toda la facturación en Holded, pero para el inventario necesitamos algo que Holded no puede hacer. ¿Podéis construir algo que se conecte?"
La respuesta fue sí. Pero "conectar" un ERP con una app web a medida es como montar IKEA sin instrucciones: las piezas existen, pero las sorpresas están en los detalles.
Esta es la historia de cómo sincronizamos 466 productos, 195 proveedores y 557 lotes entre un ERP y una aplicación web — en tiempo real, sin perder un solo dato.
Productos y lotes sincronizados bidireccionalmente.
0 errores. 0 duplicados. 0 datos perdidos.
El punto de partida: dos mundos que no se hablan
El cliente es una distribuidora de dispositivos médicos. Cada producto lleva trazabilidad obligatoria: número de lote, fecha de caducidad, proveedor de origen. Normativa europea, sin atajos.
Usaban un ERP para facturación y contabilidad. Funcionaba bien para eso. Pero para gestión de inventario con lotes, escaneo rápido y trazabilidad sanitaria, se quedaba corto.
Necesitaban un sistema propio que:
Suena razonable. "Solo hay que usar la API". ¿Verdad?
La realidad de las APIs de ERPs
Todas las APIs de ERPs tienen documentación. Algunas incluso tienen documentación correcta. La del ERP de este cliente no era una de ellas.
Estas son algunas de las trampas reales que encontramos — y que perdimos horas descubriendo:
lots lo envías como variants. Sin aviso.{"$oid": "..."}. El mismo campo. La misma API.0. Literalmente. Cero.Cada una de estas trampas es un bug en producción esperando a ocurrir. Y ninguna está en la documentación oficial.
La regla de oro de las integraciones
Nunca confíes en la documentación de una API externa al 100%. Siempre valida con datos reales antes de escribir la primera línea de código. Los campos opcionales a veces son obligatorios, los tipos cambian entre endpoints, y los errores silenciosos son la norma.
Cómo lo resolvimos (sin perder la cabeza)
La clave fue diseñar la importación como un pipeline de 4 pasos secuenciales, cada uno validado antes de pasar al siguiente:
Cada paso se ejecuta con streaming en tiempo real — el usuario ve una barra de progreso avanzando producto por producto. Nada de "espere 3 minutos mirando un spinner".
// 🔄 Streaming SSE — progreso en tiempo real
const stream = new ReadableStream({
async start(controller) {
for (const product of products) {
await syncProduct(product) // 💾 Sync individual
sendProgress(controller, i, total) // 📊 Notifica al cliente
}
controller.close() // ✅ Fin del stream
}
})
El cliente ve exactamente qué está pasando en cada momento. Si algo falla en el producto 312 de 466, lo sabe al instante — no después de esperar a que termine todo.
El truco que evitó duplicados
El mayor riesgo en una sincronización bidireccional es crear duplicados. Un producto importado dos veces. Un lote que se cuenta doble. Un proveedor que aparece tres veces con nombres ligeramente distintos.
La solución: cada registro local guarda el ID del ERP. Antes de crear, siempre buscamos primero.
// 🔍 Upsert inteligente — nunca duplica
const existing = await db
.from("products")
.select("id")
.eq("erp_id", erpProduct.id) // 🆔 ¿Ya lo tenemos?
.maybeSingle()
if (existing) {
await db.from("products")
.update(mapped) // ♻️ Actualiza
.eq("id", existing.id)
} else {
await db.from("products")
.insert(mapped) // 🆕 Crea
}
Es un patrón simple, pero la diferencia entre implementarlo bien e implementarlo mal es la diferencia entre un sistema fiable y una pesadilla de datos corruptos.
La sincronización continua: el cron que vigila
La importación inicial es solo el principio. Después, ambos sistemas necesitan mantenerse sincronizados. Nuevos productos, lotes que se agotan, proveedores que cambian de nombre.
Implementamos un cron job cada 15 minutos que consulta los cambios en el ERP y los sincroniza automáticamente. Sin intervención manual. Sin botones que pulsar.
Lo que NO hicimos
- Usar un middleware genérico tipo Zapier
- Hacer CSV exports manuales
- Duplicar la lógica de facturación
- Confiar ciegamente en la documentación
- Sincronizar todo de golpe en un batch
- Integración directa API → Base de datos
- Normalización de datos en cada dirección
- Streaming con progreso en tiempo real
- Validación exhaustiva campo por campo
- Cron de polling cada 15 minutos
La diferencia entre Zapier y una integración custom es la diferencia entre un puente colgante y el Golden Gate. El primero funciona… hasta que no. El segundo aguanta terremotos.
Lo que aprendimos
💡 Lecciones de una integración real
TL;DR
Un cliente necesitaba inventario con trazabilidad sanitaria conectado a su ERP de facturación. No existe un plugin para eso.
Construimos una integración bidireccional a medida que importó el catálogo completo (466 productos, 195 proveedores, 557 lotes) y mantiene ambos sistemas sincronizados cada 15 minutos — sin duplicados, sin datos perdidos, con progreso en tiempo real.
Las APIs de ERPs mienten en su documentación. Pero si sabes dónde mirar, puedes hacer que cualquier sistema hable con cualquier otro.
🛠️ Stack utilizado en este proyecto
¿Necesitas conectar tu ERP con un sistema propio? En Nexus Code desarrollamos integraciones a medida con cualquier ERP: Holded, Odoo, SAP, Sage, A3. Sin datos perdidos, sin duplicados. Cuéntanos tu caso y te decimos cómo lo haríamos.