StackPractices
intermediate Por Mathias Paulenko

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

  1. Handler declara la interfaz para manejar peticiones y acceder al siguiente handler
  2. Concrete Handler procesa peticiones de las que es responsable o las reenvia
  3. 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

  1. 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.

  2. Siempre llama next() o explícitamente short-circuit. Nunca dejes middleware colgado sin llamar next() o enviar una respuesta.

  3. Maneja errores gracefulmente. Cada middleware debería capturar y manejar sus propios errores, o envolverlos apropiadamente para prevenir fallo de cadena.

  4. Mantén middleware stateless. Evita almacenar estado mutable en middleware compartido entre peticiones. Usa estado scoped a la petición en su lugar.

  5. Documenta el orden de middleware. Documenta claramente el orden esperado de middleware y cualquier dependencia entre ellos.

  6. Usa async/await para operaciones async. Siempre usa patrones async/await para middleware async para evitar callback hell y asegurar manejo de errores apropiado.

  7. Proporciona un handler por defecto. Siempre incluye un handler comodín al final de la cadena para manejar peticiones que caen a través.

  8. 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.

  9. Evita referencias circulares. Asegura que la estructura de cadena sea acíclica para prevenir loops infinitos durante el procesamiento de peticiones.

  10. 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.