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
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
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
RecipeWebSockets para Comunicación en Tiempo Real
Construye comunicación bidireccional en tiempo real con WebSockets, manejando gestión de conexiones, reconexión y fallbacks.