Chain of Responsibility para Middleware de Procesamiento
Pasa peticiones a lo largo de una cadena de handlers donde cada handler decide si procesa la peticion o la pasa al siguiente handler en la pipeline
El Chain of Responsibility pattern pasa peticiones a lo largo de una cadena de handlers. Cada handler decide si procesa la peticion o la pasa al siguiente handler en la cadena. Este pattern desacopla emisores de receptores, permitiendo que multiples objetos manejen una peticion sin que el emisor sepa cual objeto la procesara finalmente.
Cuando Usar Esto
- Mas de un objeto puede manejar una peticion y el handler no se conoce de antemano
- Quieres emitir una peticion a uno de varios objetos sin especificar el receptor explicitamente
- El conjunto de objetos que pueden manejar una peticion deberia especificarse dinamicamente
Problema
Una peticion HTTP necesita pasar por autenticacion, rate limiting, validacion de peticiones y logging. Hardcodear esta secuencia en el router hace la pipeline rigida y dificil de extender.
Solucion
// chain/Handler.ts
interface RequestContext {
headers: Record<string, string>;
body: unknown;
path: string;
method: string;
user?: { id: string; roles: string[] };
}
type NextFunction = () => void;
abstract class MiddlewareHandler {
protected next: MiddlewareHandler | null = null;
setNext(handler: MiddlewareHandler): MiddlewareHandler {
this.next = handler;
return handler;
}
handle(req: RequestContext, next: NextFunction): void {
if (this.canHandle(req)) {
this.process(req, () => {
if (this.next) {
this.next.handle(req, next);
} else {
next();
}
});
} else if (this.next) {
this.next.handle(req, next);
} else {
next();
}
}
protected abstract canHandle(req: RequestContext): boolean;
protected abstract process(req: RequestContext, next: NextFunction): void;
}
// Concrete Handlers
class AuthMiddleware extends MiddlewareHandler {
protected canHandle(): boolean {
return true; // Siempre revisar auth
}
protected process(req: RequestContext, next: NextFunction): void {
const token = req.headers['authorization']?.replace('Bearer ', '');
if (!token) {
throw new Error('Unauthorized');
}
// Verificar token
req.user = { id: 'user123', roles: ['user'] };
next();
}
}
class RateLimitMiddleware extends MiddlewareHandler {
private requests = new Map<string, number[]>();
private readonly windowMs = 60000;
private readonly maxRequests = 100;
protected canHandle(): boolean {
return true;
}
protected process(req: RequestContext, next: NextFunction): void {
const clientId = req.headers['x-client-id'] || req.user?.id || 'anonymous';
const now = Date.now();
const window = this.requests.get(clientId) || [];
const recent = window.filter(t => now - t < this.windowMs);
if (recent.length >= this.maxRequests) {
throw new Error('Rate limit exceeded');
}
recent.push(now);
this.requests.set(clientId, recent);
next();
}
}
class ValidationMiddleware extends MiddlewareHandler {
protected canHandle(req: RequestContext): boolean {
return req.method === 'POST' || req.method === 'PUT';
}
protected process(req: RequestContext, next: NextFunction): void {
if (!req.body || typeof req.body !== 'object') {
throw new Error('Invalid request body');
}
next();
}
}
class LoggingMiddleware extends MiddlewareHandler {
protected canHandle(): boolean {
return true;
}
protected process(req: RequestContext, next: NextFunction): void {
console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
next();
}
}
// Construir cadena
const auth = new AuthMiddleware();
const rateLimit = new RateLimitMiddleware();
const validation = new ValidationMiddleware();
const logging = new LoggingMiddleware();
auth.setNext(rateLimit).setNext(validation).setNext(logging);
// Uso
function handleRequest(req: RequestContext): void {
auth.handle(req, () => {
console.log('Peticion alcanzo el handler final');
});
}
Como Funciona
- Handler declara la interfaz para manejar peticiones y acceder al siguiente handler
- Concrete Handler procesa peticiones de las que es responsable o las reenvia
- Client inicia la peticion a un handler en la cadena
Variacion: Middleware Estilo Express
// Estilo Express con funciones en lugar de clases
type Middleware = (req: RequestContext, next: NextFunction) => void;
function compose(middlewares: Middleware[]): Middleware {
return (req, finalNext) => {
let index = -1;
function dispatch(i: number): void {
if (i <= index) throw new Error('next() llamado multiples veces');
index = i;
const fn = i < middlewares.length ? middlewares[i] : finalNext;
if (!fn) return;
fn(req, () => dispatch(i + 1));
}
dispatch(0);
};
}
const pipeline = compose([
(req, next) => { console.log('Auth'); next(); },
(req, next) => { console.log('Rate limit'); next(); },
(req, next) => { console.log('Log'); next(); },
]);
Consideraciones de Produccion
- Asegura que los handlers llamen
next()para evitar que la pipeline se detenga - Considera short-circuiting (no llamar
next()) para cacheo o rechazo temprano - Manten los middleware stateless o scoped a la peticion para prevenir leaks
Errores Comunes
- Crear cadenas circulares que causan loops infinitos
- No llamar
next()en handlers async, causando que peticiones se cuelguen - Almacenar estado mutable en handlers compartidos entre peticiones concurrentes
- Olvidar manejar errores en middleware, causando rechazos de promesa no manejados
- Colocar operaciones costosas temprano en la cadena sin caché
- No proporcionar un handler por defecto al final de la cadena
- Mezclar preocupaciones dentro de un solo middleware en lugar de mantenerlos enfocados
- No documentar el orden de middleware y dependencias
- Fallar al validar datos de peticiones antes del procesamiento
- Usar el patrón de cadena cuando un condicional simple sería suficiente
Mejores Prácticas
-
Mantén middleware de responsabilidad única. Cada middleware debe manejar una preocupación específica (auth, validación, logging, etc.) para mantener claridad y testabilidad.
-
Siempre llama next() o explícitamente short-circuit. Nunca dejes middleware colgado sin llamar next() o enviar una respuesta.
-
Maneja errores gracefulmente. Cada middleware debería capturar y manejar sus propios errores, o envolverlos apropiadamente para prevenir fallo de cadena.
-
Mantén middleware stateless. Evita almacenar estado mutable en middleware compartido entre peticiones. Usa estado scoped a la petición en su lugar.
-
Documenta el orden de middleware. Documenta claramente el orden esperado de middleware y cualquier dependencia entre ellos.
-
Usa async/await para operaciones async. Siempre usa patrones async/await para middleware async para evitar callback hell y asegurar manejo de errores apropiado.
-
Proporciona un handler por defecto. Siempre incluye un handler comodín al final de la cadena para manejar peticiones que caen a través.
-
Añade logging y monitoreo. Incluye middleware de logging para trazar el flujo de petición a través de la cadena e identificar cuellos de botella o fallos.
-
Evita referencias circulares. Asegura que la estructura de cadena sea acíclica para prevenir loops infinitos durante el procesamiento de peticiones.
-
Prueba middleware en aislamiento. Escribe pruebas unitarias para cada middleware independientemente, luego pruebas de integración para la cadena completa.
Preguntas frecuentes
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
Decorator Pattern para Pipelines de Peticiones HTTP
Usa el Decorator pattern para componer preocupaciones transversales como logging, metricas y reintentos en pipelines de peticiones HTTP sin modificar logica central
PatternAbstract Factory para Familias de Componentes UI
Crea familias de objetos relacionados sin especificar clases concretas, habilitando implementaciones especificas de plataforma que comparten una interfaz comun
PatternInterpreter Pattern para Lenguajes de Expresion
Construye un interprete de lenguaje que evalua expresiones y reglas representando la gramatica como objetos componibles, util para formulas, queries y reglas de negocio
PatternVisitor Pattern para Operaciones Extensibles sobre
Separa algoritmos de los objetos sobre los que operan, permitiendo agregar nuevas operaciones sin modificar clases de elementos existentes