La modularidad es la práctica de dividir un código base de Node.js para que cada parte tenga un trabajo estrecho y bien definido y dependa de la menor cantidad posible del resto del sistema. Es fácil confundirla con "tener múltiples archivos", pero el recuento de archivos no dice nada sobre la modularidad: un proyecto con cincuenta archivos puede ser tan enredado como uno con cinco si cada archivo importa libremente a todos los demás.
La modularidad consiste en controlar qué partes de un sistema pueden depender de qué otras partes, para que las reglas de negocio no dependan de mecanismos de entrega como frameworks HTTP o bases de datos.
Por qué es importante: Un código base donde la lógica de dominio importa Express directamente no puede probarse sin un servidor, no puede reutilizarse desde una CLI o un worker, y no puede intercambiar frameworks sin tocar cada regla de negocio.
Conceptos clave:acoplamiento, cohesión, dirección de dependencia, puerto, adaptador, raíz de composición.
Cuándo usarlo: Estructurar un nuevo servicio desde el principio, decidir dónde pertenece una pieza de lógica, revisar si un cambio afectó demasiados archivos no relacionados y planificar una migración de framework o base de datos.
Limitaciones / Compromisos: La estratificación estricta añade indirección (una interfaz extra, un archivo extra) que es una ceremonia innecesaria en un prototipo de cinco rutas y solo se amortiza una vez que un código base tiene suficiente lógica que vale la pena proteger.
Temas relacionados: inyección de dependencias, el patrón de repositorio, arquitectura hexagonal, casos de uso y servicios de aplicación.
El acoplamiento es cuánto una parte de un sistema conoce y depende de otra parte; la cohesión es qué tan estrechamente las responsabilidades dentro de una parte pertenecen juntas.
El objetivo de la modularidad es un bajo acoplamiento entre las partes y una alta cohesión dentro de cada parte: las piezas que cambian por la misma razón viven juntas, y las piezas que cambian por diferentes razones no se arrastran mutuamente.
Un manejador de rutas y una regla de cálculo de impuestos cambian por razones completamente diferentes (uno cambia porque el formato de un encabezado HTTP se modificó, el otro porque la ley de una jurisdicción cambió), por lo que agruparlos en una sola función acopla dos fuentes de cambio no relacionadas.
Una analogía simple: un backend bien modularizado se comporta como un edificio con puertas claramente etiquetadas, donde el electricista nunca necesita pasar por la cocina para llegar al panel de interruptores.
Las habitaciones del edificio (módulos) tienen cada una un propósito, y los caminos entre ellas (dependencias) son deliberados, no atajos a través de paredes de carga porque era conveniente ese día.
Específicamente en un backend de Node.js, la pared de carga más común que la gente atraviesa es el framework HTTP: importar tipos Request/Response o llamar a res.json() desde lo más profundo de una pieza de lógica de negocio conecta esa lógica permanentemente a Express o Fastify, aunque la regla de negocio en sí no tenga nada que ver con HTTP.
La modularidad en la práctica es una disciplina de capas: el código se agrupa en capas, y las importaciones solo pueden fluir en una dirección entre ellas.
infraestructura/http -> aplicación -> dominio (rutas) (casos de uso) (reglas de negocio)Permitido: infraestructura importa aplicación importa dominioProhibido: dominio importa infraestructura o express
La capa de dominio contiene las reglas de negocio y no tiene ninguna importación de framework; no sabe si se la llama desde una ruta HTTP, un worker de cola de mensajes o un archivo de prueba. La capa de aplicación (a menudo llamada "casos de uso" o "servicios") orquesta la lógica de dominio para cumplir una operación específica, como "crear un pedido", tomando sus dependencias como parámetros simples en lugar de buscar singletons globales. La capa de infraestructura es donde viven los frameworks (routers de Express, clientes de Prisma, clientes HTTP para APIs de terceros), traduciendo entre los protocolos del mundo exterior y las llamadas a funciones simples de la capa de aplicación.
Un puerto es la interfaz que la capa de aplicación define para algo que necesita pero de lo que no quiere conocer la implementación concreta, más comúnmente una interfaz de repositorio para la persistencia.
// domain/ports/order-repository.ts - una promesa, no una implementaciónexport interface OrderRepository { save(order: Order): Promise<void>; findById(id: string): Promise<Order | null>;}
Un adaptador es la clase concreta en la capa de infraestructura que cumple un puerto, por ejemplo, un PostgresOrderRepository que implementa OrderRepository. La capa de aplicación solo importa la interfaz, nunca el adaptador, lo que hace que cambiar Postgres por DynamoDB sea un cambio confinado a un nuevo archivo de adaptador en lugar de una reescritura de cada caso de uso que toca los pedidos.
La conexión de puertos a adaptadores ocurre en un solo lugar: la raíz de composición, típicamente main.ts, que es el único archivo en todo el código base al que se le permite conocer tanto un caso de uso como su implementación concreta de Postgres al mismo tiempo. Todos los demás archivos dependen de interfaces; solo la raíz de composición depende de clases.
El valor de esta estratificación es más fácil de ver en lo que hace posible en lugar de lo que prohíbe. Un caso de uso sin importaciones de framework puede ser probado unitariamente pasando un repositorio falso en memoria, sin un servidor que enlace un puerto y sin una base de datos en ejecución; las pruebas que de otro modo necesitarían supertest y una base de datos de prueba se ejecutan en milisegundos. El mismo caso de uso también puede ser llamado desde un worker de cola de mensajes, un trabajo programado o un script CLI sin duplicar ninguna lógica de negocio, porque "cómo se activó la operación" y "qué hace la operación" nunca estuvieron acoplados en primer lugar.
Esa separación también determina cuán costosa es una migración de framework. Un código base donde la lógica de dominio nunca importó Express puede moverse a Fastify reescribiendo solo la capa de infraestructura; un código base donde las reglas de negocio están entrelazadas con llamadas req/res tiene que reescribir las reglas de negocio en sí, lo cual es una migración fundamentalmente más riesgosa y lenta porque la corrección y la sintaxis del framework ahora están entrelazadas.
Enfoque
Fortaleza
Debilidad
Mejor ajuste
Manejadores planos, acoplados al framework
Rápido de escribir, sin indirección, fácil de leer para una aplicación pequeña
No se puede probar sin un servidor; la migración del framework afecta la lógica de negocio
Prototipos, scripts de propósito único, < 5 rutas
En capas (dominio / aplicación / infraestructura)
Probable sin HTTP; el framework y la base de datos se vuelven intercambiables
Archivos e interfaces adicionales; excesivo para CRUD trivial
Servicios con reglas de negocio reales y una vida útil de varios años
Módulos de características con un kernel compartido
Mantiene los conceptos de dominio relacionados juntos; escala la propiedad del equipo
Requiere disciplina para evitar una carpeta "compartida" inflada
Bases de código más grandes con múltiples dominios acotados
A medida que un código base crece más allá de un puñado de rutas, la siguiente pregunta natural es cómo agrupar casos de uso, puertos y adaptadores relacionados, ¿solo por capa técnica o por módulo de características (pedidos, facturación, usuarios) que cada uno contiene su propia estratificación delgada internamente? La mayoría de los servicios de Node.js en producción convergen en lo último: una carpeta shared/ para preocupaciones genuinamente transversales (clases base de errores, una instancia de logger) y carpetas de características que poseen sus propias reglas de negocio, de modo que dos módulos nunca discuten sobre dónde pertenece una regla como el cálculo de impuestos.
La aplicación es tan importante como el patrón en sí: una regla de capas que existe solo en una página wiki se erosiona en unas pocas solicitudes de extracción. Los equipos que mantienen esta disciplina a lo largo del tiempo suelen codificarla como una regla de ESLint (no-restricted-imports que bloquea express dentro de domain/) para que una violación falle en CI en lugar de en la memoria de la revisión de código.
"La modularidad solo significa dividir el código en más archivos." El recuento de archivos es ortogonal a la modularidad; la propiedad que importa es qué archivos pueden importar qué otros archivos, no cuántos archivos existen.
"Las interfaces (puertos) son una sobrecarga innecesaria si solo uso una base de datos." El valor no es el hipotético intercambio de bases de datos, es la capacidad de prueba: un puerto permite que un caso de uso se pruebe con un falso en memoria, con o sin cambiar de base de datos.
"La inyección de dependencias requiere un framework o un contenedor." Pasar dependencias como parámetros de función o constructor simples es inyección de dependencias; un contenedor como Awilix o el sistema DI de NestJS es una implementación de la idea, no un requisito previo para ella.
"La estratificación ralentiza todos los proyectos, por lo que no vale la pena al principio." Es un costo real en un prototipo de cinco rutas y un ahorro real en un servicio con años de reglas de negocio por delante; el compromiso depende de la vida útil esperada y la densidad de la lógica, no de una regla universal.
"Una carpeta shared/ es donde pertenece todo lo reutilizable." Reutilizable y transversal no son lo mismo: la lógica de negocio que dos módulos de características necesitan debe ser propiedad de uno de ellos y exponerse como un servicio, no arrojarse a un archivo de utilidades compartido.
¿Qué significa realmente "modularidad" para un backend de Node.js?
Controlar la dirección de las dependencias para que la lógica de negocio no dependa de los mecanismos de entrega (frameworks HTTP, bases de datos); es una propiedad del grafo de dependencias, no un recuento de archivos o carpetas.
¿Por qué la lógica de dominio no debería importar tipos de Express o Fastify?
Porque eso acopla la corrección de una regla de negocio a la sintaxis de un framework específico; la regla se vuelve imposible de probar sin un servidor e inamovible sin reescribirla durante cualquier migración de framework.
¿Cuál es la diferencia entre un "puerto" y un "adaptador"?
Un puerto es una interfaz que la capa de aplicación define describiendo lo que necesita, sin decir cómo; un adaptador es la clase concreta en la capa de infraestructura que realmente cumple esa interfaz, como una implementación de repositorio respaldada por Postgres.
¿Cómo se aplica realmente la dirección de las dependencias en el día a día?
Principalmente por convención más una regla de ESLint como no-restricted-imports que bloquea las importaciones de frameworks dentro de la capa de dominio en CI; sin una aplicación automatizada, las reglas de capas tienden a erosionarse en unas pocas solicitudes de extracción.
¿Qué es una "raíz de composición" y por qué solo hay una?
Es el único lugar en el código base, típicamente main.ts, al que se le permite conocer tanto una interfaz como su implementación concreta al mismo tiempo, conectándolas; mantener ese conocimiento en un solo lugar es lo que permite que todos los demás archivos dependan solo de interfaces.
¿Cada proyecto de Node.js necesita tanta estratificación?
No, un prototipo de cinco rutas paga el costo de las interfaces y las capas sin obtener el beneficio, ya que aún no hay una migración de framework o una lógica de negocio compleja que proteger; el compromiso cambia a medida que crecen la densidad de las reglas de negocio y la vida útil esperada.
¿Cómo hace esta estratificación que las pruebas sean más rápidas?
Un caso de uso que toma sus dependencias como parámetros puede probarse pasando un falso en memoria en lugar de una base de datos real o un servidor HTTP en ejecución, por lo que las pruebas se ejecutan en milisegundos y no requieren E/S de red ni enlace de puertos.
¿El código relacionado debe agruparse por capa técnica o por característica?
Las bases de código más grandes suelen agruparse por característica (pedidos, facturación) y cada característica posee su propia estratificación delgada de dominio/aplicación/infraestructura internamente; la agrupación puramente por capa técnica tiende a dispersar la lógica de una característica en demasiadas carpetas de nivel superior a medida que un proyecto crece.
¿La inyección de dependencias es lo mismo que un contenedor DI?
No, la inyección de dependencias es la práctica general de pasar dependencias en lugar de importar singletons; un contenedor (Awilix, NestJS) automatiza el cableado por ti, pero la inyección manual de constructor o parámetro también es DI.
¿Cuál es el riesgo de una carpeta `shared/` inflada?
Tiende a acumular lógica de negocio que en realidad pertenece a una característica específica, lo que vuelve a acoplar módulos que se suponía que eran independientes; la solución suele ser que un módulo de características posea la lógica y la exponga como un servicio al otro.
¿Qué tan costosa es una migración de framework en un código base bien estratificado versus uno acoplado?
En un código base estratificado, solo la capa de infraestructura (rutas, adaptadores) necesita reescribirse; en un código base acoplado, las reglas de negocio mismas hacen referencia al framework, por lo que la migración tiene que tocar y volver a verificar el código crítico para la corrección, no solo la fontanería.