Patrones de Composicion de Middleware en Express.js
Construye aplicaciones Express mantenibles usando patrones de composicion de middleware para autenticacion, validacion, manejo de errores, propagacion de contexto y wrappers de rutas async
El middleware de Express es la columna vertebral de la arquitectura de APIs en Node.js, pero cadenas de middleware profundamente anidadas o duplicadas rapidamente se vuelven inmantenibles. Esta recipe cubre patrones de composicion para autenticación, validación, manejo de errores, propagacion de contexto de request y wrappers de rutas async que mantienen los route handlers limpios y testeables.
Cuando Usar Esto
- Las rutas de Express acumulan middleware repetitivo (auth, logging, validacion) copiado en todos lados
- Los route handlers async lanzan unhandled promise rejections que crashean el servidor. Consulta Manejo de Errores para patrones.
- Necesitas contexto scopeado a la request (usuario, trace ID) accesible a traves de todo el call stack
Solucion
1. Wrapper de Rutas Async
// middleware/asyncHandler.ts
import { Request, Response, NextFunction, RequestHandler } from 'express';
type AsyncRequestHandler = (req: Request, res: Response, next: NextFunction) => Promise<unknown>;
function asyncHandler(fn: AsyncRequestHandler): RequestHandler {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Uso — no se necesita try/catch
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await userService.findById(req.params.id);
res.json(user);
}));
2. Factory de Middleware Componible
// middleware/compose.ts
import { Request, Response, NextFunction, RequestHandler } from 'express';
type Middleware = RequestHandler | [RequestHandler, ...RequestHandler[]];
function compose(...middlewares: Middleware[]): RequestHandler[] {
return middlewares.flatMap((m) => Array.isArray(m) ? m : [m]);
}
const authenticated = compose(verifyToken, requireActiveUser);
const validated = (schema: ZodSchema) => compose(validateBody(schema));
app.post('/posts', ...compose(authenticated, validated(createPostSchema)), asyncHandler(createPost));
app.patch('/posts/:id', ...compose(authenticated, validated(updatePostSchema)), asyncHandler(updatePost));
3. Propagacion de Contexto de Request
// middleware/context.ts
import { AsyncLocalStorage } from 'async_hooks';
import { Request, Response, NextFunction } from 'express';
interface RequestContext {
traceId: string;
user?: { id: string; role: string };
startTime: number;
}
const asyncStorage = new AsyncLocalStorage<RequestContext>();
function contextMiddleware(req: Request, res: Response, next: NextFunction): void {
const context: RequestContext = {
traceId: req.headers['x-trace-id'] as string || crypto.randomUUID(),
startTime: Date.now(),
};
asyncStorage.run(context, () => {
res.setHeader('X-Trace-Id', context.traceId);
next();
});
}
function getContext(): RequestContext | undefined {
return asyncStorage.getStore();
}
class UserService {
async findById(id: string): Promise<User> {
const ctx = getContext();
logger.info('Fetching user', { traceId: ctx?.traceId, userId: id });
return await db.users.findById(id);
}
}
4. Handler de Errores Unificado
// middleware/errorHandler.ts
import { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
class AppError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string
) {
super(message);
this.name = 'AppError';
}
}
const errorHandler: ErrorRequestHandler = (err, req, res, _next) => {
const ctx = getContext();
if (err instanceof AppError) {
res.status(err.statusCode).json({
error: err.message,
code: err.code,
traceId: ctx?.traceId,
});
return;
}
if (err.name === 'ValidationError') {
res.status(400).json({
error: 'Validation failed',
details: err.errors,
traceId: ctx?.traceId,
});
return;
}
logger.error('Unhandled error', { traceId: ctx?.traceId, error: err });
res.status(500).json({
error: 'Internal server error',
traceId: ctx?.traceId,
});
};
app.use(errorHandler);
5. Middleware de Validacion con Zod
// middleware/validate.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';
function validateBody(schema: ZodSchema) {
return (req: Request, res: Response, next: NextFunction): void => {
const result = schema.safeParse(req.body);
if (!result.success) {
res.status(400).json({
error: 'Validation failed',
issues: result.error.issues,
});
return;
}
req.body = result.data;
next();
};
}
function validateParams(schema: ZodSchema) {
return (req: Request, res: Response, next: NextFunction): void => {
const result = schema.safeParse(req.params);
if (!result.success) {
res.status(400).json({ error: 'Invalid parameters', issues: result.error.issues });
return;
}
req.params = result.data;
next();
};
}
Como Funciona
- Wrappers async capturan promises rechazadas y las reenvian a handlers de error de Express
- Composicion aplana arrays de middleware anidados en stacks limpios y reutilizables
- AsyncLocalStorage crea contexto de request implicito sin propagacion manual a traves de cada firma de funcion
- Validacion tipada transforma y acota datos de request en el boundary antes de que los route handlers ejecuten
Consideraciones de Produccion
- Registra handlers de error al final del stack de middleware (despues de todas las rutas)
- No llames
next()despues de enviar una respuesta; causa errores “headers already sent” - Usa
res.on('finish')para middleware de logging para capturar el status de respuesta actual
Errores Comunes
- Llamar
next()dentro de middleware async sin await, causando race conditions - Olvidar llamar
next()en middleware sincronico, colgando requests indefinidamente - Lanzar strings en lugar de objetos Error, perdiendo stack traces
Mejores Prácticas
- El orden importa: registra middleware en la secuencia correcta — logging primero, luego autenticación, luego autorización, luego rate limiting, luego lógica de negocio. Un stack mal ordenado puede permitir peticiones no autenticadas a endpoints costosos.
- Mantén middleware delgado: cada middleware debería hacer una sola cosa. Middleware delgado es más fácil de testear, reutilizar y debuggear.
- Usa
express.Router()para stacks modulares: monta routers con diferentes stacks de middleware para diferentes grupos de rutas. Rutas de API get middleware de auth; rutas públicas get solo logging y CORS. - Siempre maneja errores async: wrappea middleware async en try/catch y pasa errores a
next(err). - Setea
req.localspara data compartida: pasa data entre middleware usandoreq. localsen lugar de mutarreqdirectamente. Esta es la convención de Express y funciona con la mayoría de middleware de terceros.
Checklist de Producción
- Orden de middleware es: logging → CORS → auth → rate limiting → routes → error handler
- Error-handling middleware está registrado último (función con 4 args)
- Middleware async usa error wrapper o paquete
express-async-errors - Headers sensibles de request se stripped antes de loguear
- CORS middleware está configurado con allowlist explícita de origins, no
* - Body parser tiene límites de tamaño configurados (e.g., 1MB JSON, 10MB multipart)
- Helmet middleware está habilitado para security headers
- Request ID se genera y se attachea a
req.localspara tracing - Endpoint de health check (
/health) bypassa auth middleware - Graceful shutdown drena requests in-flight antes de cerrar
Consideraciones de Escalado
- Overhead de middleware a escala: cada middleware agrega 0. 1-1ms por petición. Con 10 funciones middleware, eso es 1-10ms de overhead antes de la lógica de negocio.
- Memory leaks en procesos long-running: middleware que acumula estado (caches, connection pools) puede leakear memoria over días/semanas.
- Escalado horizontal: middleware de Express corre por instancia. Middleware stateful (sessions, rate limiting) necesita shared storage (Redis, Memcached) al escalar a múltiples instancias. Middleware stateless (logging, CORS) funciona sin cambios.
Cuándo No Usar Este Enfoque
- APIs de alto rendimiento (>50K req/s): Express agrega overhead de la ejecución de la middleware chain y el JavaScript runtime.
- Funciones serverless: las middleware chains de Express no cold-startean eficientemente en Lambda.
- Servir archivos estáticos simple: si tu app solo sirve archivos estáticos, Express middleware es excesivo.
Estrategia de Testing
- Unit test middleware en aislamiento: crea una app Express minimal con
supertest, monta solo el middleware bajo test y haz HTTP requests. Asserta sobre response status, headers y body. Mockeanext()para verificar call order. - Integration test el stack completo de middleware: Verifica middleware ordering, error handling y response transformations.
- Testea error paths explícitamente: envía peticiones malformadas, triggerea timeouts y simula downstream failures. Verifica que error-handling middleware catchee y formatee errores correctamente.
- Performance test middleware overhead: Target <5ms total middleware overhead por petición.
Estimación de Costos
| Componente | Costo | Notas |
|---|---|---|
| Express (self-hosted) | $0 | Open-source, MIT license |
| PM2 cluster mode | $0 | Process manager, open-source |
| Redis (para session/rate limit) | $10-$75/mes | Shared state across instancias |
| Load balancer | $20-$100/mes | AWS ALB, GCP LB, Nginx |
| Monitoring (PM2 Plus) | $0-$80/mes | PM2 Plus, Datadog APM |
Para 10K req/s: 2x EC2 t3.large ($60/mes) + Redis ($15/mes) + ALB ($25/mes) = ~$100/mes. PM2 cluster mode es free. Agrega Datadog APM ($80/mes) para monitoring de producción.
Monitoring y Observabilidad
- Trackea execution time de middleware por petición: Alerta si cualquier middleware excede 10ms p95.
- Monitorea error rates de middleware: cuenta errores por función middleware. Setea alertas para error rate >1% en middleware critical (auth, CORS, rate limiting).
- Loggea violaciones de middleware order: si middleware ejecuta out of order (e. g. , auth después de body parsing), loggea un warning. Bugs de middleware order son hard to debug en producción.
- Trackea memory usage por middleware: algún middleware (body-parser, session) allocatea memoria por petición.
Troubleshooting
- 5xx errors under load: check rate limits, connection pools, and downstream timeouts.
- CORS errors in the browser: confirm allowed origins, methods, and headers. Preflight requests must return the right headers before the actual request.
- Unexpected 404s: verify route definitions, path parameters, and base paths. Watch for trailing slashes and URL encoding differences.
- Authentication failures: validate token expiry, signature algorithms, and clock skew. Log rejected tokens without exposing secrets.
- Slow response times: profile the slowest percentiles.
Referencia Rápida
- Comando principal: ejecuta la solución base del artículo y verifica el resultado esperado.
- Validación: confirma que los tests pasan y que las métricas clave no se degradaron.
- Rollback: si algo falla, revierte el cambio y consulta la sección de Troubleshooting.
Lectura Adicional
- Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
- Guías relacionadas: explora las guías de express y nodejs para profundizar.
- Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
- Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.
Notas de Producción
- Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
- Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
- Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
- Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.
Puntos Clave
- Aplica patrones de composicion de middleware en express.js cuando necesites una solución práctica para tu caso de uso.
- Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
- Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.
Errores Comunes en Producción
- Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
- Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
- Codificar valores fijos que deberían ser configurables por entorno.
- Olvidar agregar logging y monitoreo en cada paso.
- Desplegar sin plan de rollback ni estrategia de backup probada.
- Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
- No documentar la versión y configuración usadas en producción.
- Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.
Preguntas frecuentes
Deberia usar Express o Fastify para proyectos nuevos?
Fastify ofrece mejor performance y validacion de schema built-in. Express tiene ecosistema mas grande y familiaridad. Ambos son viables para produccion. Para APIs en Go, consulta Go REST API con Gin.
Como testeo middleware en aislamiento?
Crea una mini app Express en tests, monta el middleware y haz requests con supertest contra ella.
Recursos Relacionados
REST API en Go con Gin y Middleware
Construye APIs REST listas para producción en Go usando el framework Gin con middleware custom para logging, autenticación, validación y manejo de errores.
RecipeValidacion de Datos Basada en Schemas con Zod en TypeScript
Valida y sanitiza datos entrantes usando schemas Zod con inferencia de TypeScript, refinements custom y formateo de errores para validacion robusta de APIs y formularios
RecipeCrea un servidor WebSocket (Python, Node.js, Java)
Cómo construir un servidor WebSocket para comunicación en tiempo real bidireccional, con gestión de conexiones, difusión de mensajes y heartbeats.