Tu código es una fortaleza: por qué la arquitectura Hexagonal salvará tu proyecto JavaScript
\n\n¿Alguna vez te has enfrentado a una situación en la que cambiar de base de datos o reemplazar una API externa se convertía en un infierno de reescribir la mitad del proyecto? ¿O quizás escribías pruebas unitarias que se parecían más a pruebas de integración porque la lógica de negocio estaba fuertemente acoplada a Express o MongoDB? Si es así, bienvenido al mundo de la arquitectura Hexagonal (Puertos y Adaptadores).
\n\nNo es solo un patrón de moda de los libros de Alistair Cockburn. Es una forma práctica de hacer que tu código JavaScript sea robusto, comprobable e independiente de los frameworks. En este artículo no daremos aburridas conferencias. Tomaremos una tarea real (un sistema de gestión de pedidos) y la construiremos paso a paso basándonos en los principios de la arquitectura hexagonal en JavaScript. Verás cómo la lógica de negocio se mantiene inmaculadamente limpia, mientras que todo el «ruido» (bases de datos, solicitudes HTTP, colas) queda fuera.
\n\n¿Listo? ¡Allá vamos!
\n\n¿Qué es la arquitectura Hexagonal (y qué tienen que ver los puertos y adaptadores)?
\n\nImagina una esclusa (gateway) de una nave espacial. Dentro, está el panel de control (tu lógica de negocio). Fuera, hay un número infinito de planetas diferentes (bases de datos, frameworks web, servicios externos). La esclusa es el puerto (interfaz). Y el traje espacial que te permite respirar en un planeta concreto es el adaptador.
\n\nLa idea principal: el núcleo de la aplicación (core) no debe saber quién ni cómo se comunica con él. Solo declara «puertos» (interfaces). Y las implementaciones concretas (adaptadores) se conectan desde fuera. Esto proporciona tres superpoderes:
\n\n- \n
- Aislamiento de la lógica de negocio — cambias la base de datos sin tocar el código de cálculo de descuentos. \n
- Capacidad de prueba — pruebas la lógica sin levantar Docker ni una base de datos real. \n
- Independencia del framework — puedes migrar de Express a Fastify simplemente escribiendo un nuevo adaptador. \n
Pasemos al código.
\n\nConfiguración del proyecto y estructura de carpetas
\n\nPara empezar, crearemos una estructura simple. Escribiremos en TypeScript puro (o JavaScript con JSDoc, según prefieras). Nuestro objetivo es un sistema de gestión de pedidos.
\n\nsrc/\n├── core/\n│ ├── domain/ # Entidades y Value Objects\n│ ├── ports/ # Interfaces (puertos)\n│ └── services/ # Lógica de negocio (casos de uso)\n├── adapters/\n│ ├── primary/ # Adaptadores que llaman a nuestro núcleo (HTTP, CLI)\n│ └── secondary/ # Adaptadores que el núcleo llama (BD, APIs externas)\n└── app.js # Punto de entrada (ensamblaje de todo)\n\n\nPaso 1. Núcleo: Entidades de dominio y puertos
\n\nEmpezamos por lo más importante: lo que no depende de nada. Crearemos la entidad Order (pedido).
core/domain/Order.js
\n\nexport class Order {\n constructor({ id, userId, items, status = 'pending' }) {\n this.id = id;\n this.userId = userId;\n this.items = items; // Array { productId, quantity, price }\n this.status = status;\n this.total = this.calculateTotal();\n }\n\n calculateTotal() {\n return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);\n }\n\n approve() {\n if (this.status !== 'pending') {\n throw new Error('Only pending orders can be approved');\n }\n this.status = 'approved';\n }\n}\n\n\nAhora definimos los puertos: las interfaces que nuestro núcleo espera del mundo exterior.
\n\ncore/ports/OrderRepository.js
\n\n// Esto es un puerto (interfaz). No lo implementamos aquí.\nexport class OrderRepository {\n async save(order) { throw new Error('Not implemented'); }\n async findById(id) { throw new Error('Not implemented'); }\n}\n\n\ncore/ports/EmailService.js
\n\nexport class EmailService {\n async sendOrderConfirmation(order) { throw new Err