Antipatrones de Express.js con contexto | Nicolás Garzón
Un antipatrón no es una regla prohibida: es una solución recurrente que parece cómoda, pero genera consecuencias previsibles cuando el sistema crece o cambia de contexto.
La pregunta útil no es “¿está permitido?”, sino:
Texto
Copiar ¿Qué problema intentaba resolver?
→ ¿Qué coste introduce?
→ ¿En qué escala aparece?
→ ¿Cuál es la alternativa proporcional?Una API de una tarde y un sistema multi-tenant de pedidos no necesitan la misma estructura. El criterio evita tanto el caos como la arquitectura ceremonial.
TypeScript
Copiar router. post ( '/' , async ( request, response) => {
} ) ; Es la ruta más directa para un CRUD inicial.
HTTP, negocio, persistencia y efectos quedan unidos. La operación no puede reutilizarse desde jobs, es difícil probar reglas y cada cambio agranda el handler.
Handler de cientos de líneas.
SQL y SDKs externos en la ruta.
Tests solo end-to-end.
Request/response pasan por toda la aplicación.
Extrae un caso de uso cuando aparece coordinación o regla real. Mantén el handler como traductor HTTP.
Un prototipo o endpoint trivial de lectura puede permanecer pequeño en una ruta. La clave es reconocer cuándo dejó de ser trivial.
Mover el mismo código de route a controller no cambia el problema.
Texto
Copiar router → controller enormeEl controller sigue mezclando validación, transacción, negocio y serialización.
Divide por operación y responsabilidad, no solo por nombre de archivo.
TypeScript
Copiar class OrderService {
findById ( id) {
return orderRepository. findById ( id) ;
}
} Añade navegación sin introducir decisión, boundary o contrato.
Usa el repository directamente o crea un caso de uso con intención real como CancelOrder.
Un service vale cuando coordina reglas, dependencias o transacciones, no porque una plantilla lo exige.
TypeScript
Copiar router. post ( '/' , checkStock, calculateTotal, reserveInventory, handler) ; La regla depende de Express, el orden es implícito y otros entry points no pueden reutilizarla.
Middleware para concerns del boundary: auth, parsing, validation, request context. Caso de uso para stock, total y reserva.
Añadir decenas de propiedades a req crea dependencias invisibles:
TypeScript
Copiar request. user
request. tenant
request. order
request. permissions
request. transactionres.locals tipado o inputs explícitos pueden limitar el alcance. Una conexión transaccional no debería viajar globalmente por todo el pipeline.
No recibe errores posteriores.
Express no lo reconoce como error middleware.
Oculta bugs y fallos de infraestructura.
Filtra implementación y datos.
La alternativa es un mapper por categorías, logging interno y contrato público estable.
TypeScript
Copiar response. status ( 401 ) . json ( ... ) ;
next ( ) ; Produce headers already sent, respuestas parciales o errores difíciles de rastrear.
Cada rama terminal debe retornar. Tests HTTP deben cubrir short-circuit.
Una rama olvidada deja requests colgadas. Timeouts son una red de seguridad, no la corrección.
Instrumenta finish/close y revisa todas las ramas.
TypeScript
Copiar const body = request. body as CreateOrderInput; No verifica bytes. La consecuencia puede ser crash, injection indirecta o reglas aplicadas a datos inesperados.
Boundary externo comienza como unknown y pasa por schema runtime.
TypeScript
Copiar repository. findMany ( { where: request. query } ) ; Permite filtros, relaciones o costes no controlados. Incluso sin SQL injection puede causar exposición y DoS lógico.
Define un lenguaje público, whitelist y límites.
CORS solo controla lectura desde browsers. origin: '*' no vuelve pública de forma segura una API ni protege contra scripts, curl o servidores.
Diseña auth, authorization, CSRF y rate limits por separado.
Sin límites, un cliente controla memoria, CPU, disk y DB work. Aceptar un JSON de 50 MB para una operación normal es un fallo de contrato.
Limita bytes, campos, profundidad, items, tiempo y concurrency en varias capas.
Elegir JWT “porque es stateless” puede introducir:
Revocación compleja.
Claims obsoletos.
Refresh rotation.
XSS/CSRF según almacenamiento.
Tokens grandes.
Sessions server-side pueden ser más simples para una web. Elige según clientes y threat model.
Verificar token y después cargar cualquier /orders/:id permite escalación horizontal.
Scopea la query por tenant/actor y aplica policy en el caso de uso.
Abrir una conexión nueva por request añade latencia y agota PostgreSQL. Usa pool compartido y reserva un client solo para transacciones.
Texto
Copiar BEGIN
→ lock inventory
→ esperar payment API 8s
→ COMMITRetiene conexiones y locks, genera deadlocks y reduce capacidad.
Confirma la intención local y coordina el efecto remoto con estados, outbox, idempotencia o saga.
Sessions, rate limits, jobs o idempotency keys en memoria:
Se pierden al reiniciar.
No se comparten entre réplicas.
Crecen sin límite.
Puede ser útil en desarrollo, pero producción necesita store durable/compartido o un diseño stateless real.
Funciona en una máquina, falla con múltiples instancias, despliegues y storage efímero. Usa object storage o volumen explícito; guarda metadata en DB.
Hashing pesado, reportes, imágenes o regex complejas bloquean el event loop.
Mide. Usa worker threads, jobs o servicios especializados cuando el coste lo justifique. No muevas cualquier cálculo pequeño a una cola.
TypeScript
Copiar void sendEmail ( ) ;
response. sendStatus ( 201 ) ; El proceso puede caer, el error queda sin manejar y el efecto puede perderse. Usa outbox/queue para trabajo importante.
Reintentar cualquier fallo puede duplicar pagos, amplificar caídas y ocultar errores permanentes.
Clasifica retryability, usa idempotencia, backoff, jitter y deadline.
Una key como order:123 puede mezclar tenants. Cachear authorization o stock sin política puede filtrar o vender de más.
Incluye scope y version; conserva invariantes en la fuente de verdad.
Permite spoofing de IP/protocolo si existe acceso directo o cadena no controlada. Confía solo en proxies/red conocidos.
Facilita debugging inmediato y crea una fuga de secretos/PII a largo plazo. Estructura eventos y redacta centralmente.
Aplicar Clean Architecture completa a tres rutas puede crear:
Interfaces vacías.
Mappers idénticos.
Navegación excesiva.
Desarrollo lento.
La alternativa no es mezclar todo: empieza con módulos compactos y separa por razones de cambio reales.
Separar deployment añade red, observabilidad, consistencia distribuida y operación. Primero mejora límites modulares dentro del monolito.
Extrae un servicio cuando existe autonomía, escala, seguridad o lifecycle realmente distinto.
Son lentos y difíciles de diagnosticar. Sin unit/integration, cada regla requiere infraestructura completa.
Combina capas según el riesgo: unit para reglas, integration HTTP/DB para wiring, E2E para flujos críticos.
Una liveness que depende de DB, Redis y proveedores causa restart storms. Liveness verifica proceso; readiness, capacidad de atender.
Durante deploy se cortan requests y jobs. Implementa not-ready, drain, cleanup y deadline.
Añadir cache, cluster, índices o más pool puede empeorar. Usa métricas, traces, profiles y load tests para encontrar el cuello.
Observas un handler de 80 líneas. Antes de extraer preguntas:
¿Contiene reglas o solo traducción?
¿Se reutiliza fuera de HTTP?
¿Tiene dependencias externas?
¿Necesita transacción?
¿Qué parte cambia por una razón distinta?
La respuesta determina si conviene función auxiliar, caso de uso, repository o dejarlo como está.
Añade tests del comportamiento actual.
Identifica un boundary doloroso.
Extrae una operación con inputs explícitos.
Mantén el contrato HTTP.
Migra una ruta.
Mide complejidad y velocidad.
Repite solo donde aporta.
Evita una reescritura total motivada por estética.
Cambios frecuentes rompen áreas no relacionadas.
No se pueden probar reglas sin servidor.
Dependencias globales impiden aislamiento.
Incidentes no pueden diagnosticarse.
Concurrencia produce inconsistencias.
Nuevos entry points duplican lógica.
Estas señales justifican boundaries adicionales.
Un antipatrón depende del contexto y sus consecuencias.
Mover código de archivo no crea separación.
HTTP, negocio y persistencia necesitan límites proporcionales.
Seguridad y operación no son paquetes instalados al final.
La alternativa correcta es la mínima que reduce un riesgo real.
Refactoriza con tests y de forma incremental.
¿Por qué un service que delega una línea puede ser ruido?
¿Cuándo middleware es el lugar correcto?
¿Qué problema crea una transacción alrededor de una API externa?
¿Por qué MemoryStore funciona en desarrollo pero no al escalar?
¿Cómo distinguir simplicidad de falta de arquitectura?
Ver respuestas
No introduce decisión, contrato o boundary.
Para concerns del lifecycle HTTP reutilizables, no reglas de negocio.
Retiene locks/conexiones y no puede revertir el efecto remoto.
El estado no se comparte y se pierde al reiniciar.
La simplicidad mantiene responsabilidades claras; la falta de arquitectura produce acoplamiento, duplicación e imposibilidad de probar/operar.
Modelo mental completo de Express.js reúne toda la ruta y funciona como guía de diseño, diagnóstico, testing y producción.