Routers y modularización por feature en Express.js | Nicolás Garzón
Un Router organiza un subconjunto del pipeline HTTP. Modularizar por feature mantiene juntos contratos, validación y handlers relacionados sin convertir la estructura en carpetas globales desconectadas.
express.Router() crea un handler modular con su propio stack de middleware y rutas:
TypeScript
Copiar import { Router } from 'express' ;
export const ordersRouter = Router ( ) ;
ordersRouter. get ( '/' , listOrdersHandler) ;
ordersRouter. post ( '/' , validateCreateOrder, createOrderHandler) ; Después se monta con un prefijo:
TypeScript
Copiar app. use ( '/api/orders' , ordersRouter) ; El router no necesita conocer el prefijo global. Esto permite reutilizarlo, versionarlo o probarlo de forma aislada.
Una aplicación pequeña puede registrar todas las rutas en app.ts. Al crecer aparecen:
archivos enormes
conflictos de nombres
middleware global innecesario
dependencias cruzadas
dificultad para localizar un contrato
El router crea una frontera HTTP. No es por sí mismo una frontera de dominio: todavía debes decidir qué dependencias y reglas pertenecen al módulo.
Texto
Copiar app
├─ middleware global
├─ /orders → ordersRouter
│ ├─ middleware del módulo
│ ├─ GET /
│ ├─ POST /
│ └─ GET /:id
└─ /customers → customersRouterCada router recibe la URL restante después del prefijo y comparte los mismos objetos request/response.
Texto
Copiar src/
modules/
orders/
orders.router.ts
create-order.handler.ts
create-order.schema.ts
create-order.use-case.ts
order.repository.tsEsta organización mantiene cerca piezas que cambian juntas. Una estructura global:
Texto
Copiar controllers/
services/
repositories/
validators/puede obligar a navegar varias carpetas para entender una sola operación.
Evita imports globales de dependencias:
TypeScript
Copiar type OrdersRouterDependencies = {
createOrder: CreateOrder;
listOrders: ListOrders;
} ;
export function createOrdersRouter (
dependencies: OrdersRouterDependencies,
) : Router {
const router = Router ( ) ;
router. post (
'/' ,
validate ( createOrderSchema) ,
makeCreateOrderHandler ( dependencies. createOrder) ,
) ;
router. get ( '/' , makeListOrdersHandler ( dependencies. listOrders) ) ;
return router;
} La factory hace visibles dependencias y facilita tests.
TypeScript
Copiar router. use ( requireAuthentication) ; Esto protege todas las rutas registradas después. No lo uses cuando algunas rutas son públicas, o separa subrouters:
TypeScript
Copiar router. get ( '/public-catalog' , listPublicCatalog) ;
router. use ( requireAuthentication) ;
router. get ( '/private-orders' , listPrivateOrders) ; TypeScript
Copiar const branchOrdersRouter = Router ( { mergeParams: true } ) ;
app. use (
'/businesses/:businessId/branches/:branchId/orders' ,
branchOrdersRouter,
) ; mergeParams: true permite acceder a params del padre. Esa dependencia debe probarse; de lo contrario, branchId puede ser undefined.
Un módulo debería exportar pocas entradas:
TypeScript
Copiar export { createOrdersRouter } from './orders.router.js' ; Evita que otros módulos importen archivos internos arbitrariamente. Si necesitan una capacidad, crea un contrato explícito.
Texto
Copiar router
→ matching, orden y middleware
handler/controller
→ traducción HTTP
use case
→ operación del negocioUn router gigante que consulta base de datos directamente mezcla las tres responsabilidades.
Puedes montar distintas versiones:
TypeScript
Copiar app. use ( '/api/v1/orders' , createOrdersV1Router ( deps) ) ;
app. use ( '/api/v2/orders' , createOrdersV2Router ( deps) ) ; No dupliques todo el negocio. Las versiones pueden compartir casos de uso y variar mappers, schemas o contrato HTTP.
TypeScript
Copiar export function createOrdersRouter ( deps: Dependencies) : Router {
const router = Router ( ) ;
router. use ( deps. authenticate) ;
router. get (
'/' ,
validateQuery ( listOrdersSchema) ,
makeListOrdersHandler ( deps. listOrders) ,
) ;
router. post (
'/' ,
deps. authorize ( 'orders:create' ) ,
validateBody ( createOrderSchema) ,
makeCreateOrderHandler ( deps. createOrder) ,
) ;
router. get (
'/:orderId' ,
validateParams ( orderIdSchema) ,
makeGetOrderHandler ( deps. getOrder) ,
) ;
return router;
} El módulo expresa auth común y precondiciones específicas. Los handlers reciben casos de uso, no acceden a singletons ocultos.
Puede ser deliberado, pero links absolutos y auth pueden asumir un prefijo. Usa baseUrl y configuración explícita.
Solo afecta rutas posteriores.
Indican boundaries débiles. Extrae un contrato compartido o coordina desde una capa superior.
Crear un router para una sola ruta puede ser correcto si representa un módulo real, pero no es obligatorio.
Si mezcla catálogo, pagos, usuarios y reportes, probablemente la frontera sea demasiado amplia.
Dividir por tipo técnico y no por feature.
Importar un pool global desde cada handler.
Usar mergeParams sin verificar relaciones padre-hijo.
Ocultar autorización dentro de un router helper difícil de rastrear.
Crear barrel files que producen ciclos.
Duplicar lógica de negocio al versionar rutas.
Prueba el router montado en una app mínima:
rutas correctas
prefijo
middleware común
params heredados
404 dentro y fuera del router
dependencias falsas
versión del contrato
Más módulos reducen archivos grandes, pero aumentan navegación y composición. La unidad adecuada es una responsabilidad que cambia junta y tiene un vocabulario propio, no una cifra fija de rutas.
Router es una miniaplicación HTTP, no todo el dominio.
El prefijo se define al montar.
Factories hacen visibles las dependencias.
Organizar por feature mantiene contexto.
mergeParams crea una dependencia explícita del padre.
Versiona el contrato sin duplicar innecesariamente el negocio.
¿Por qué una factory de router mejora tests?
¿Qué diferencia existe entre router y caso de uso?
¿Cuándo necesitas mergeParams?
¿Qué problema puede indicar una dependencia circular entre routers?
Ver respuestas
Permite inyectar dependencias falsas y construir el módulo sin globals.
El router coordina HTTP; el caso de uso coordina negocio.
Cuando un router hijo necesita parámetros capturados por el padre.
Boundaries de módulos poco claros o responsabilidades mezcladas.
Controllers y handlers HTTP explica cómo traducir entre el contrato de transporte y la aplicación.