Pattern Chain of Responsibility
Passez une requête le long d'une chaîne de handlers. Chaque handler décide de traiter la requête ou de la passer au suivant.
Le Problème
Vous développez une API. Chaque requête a besoin d'authentification, validation, rate limiting et logging. Mais tout est mélangé :
async function handleRequest(req: Request): Promise<Response> {
// Auth mélangée
const user = verifyToken(req.headers.authorization);
if (!user) return new Response("Unauthorized", { status: 401 });
// Rate limiting mélangé
if (isRateLimited(user.id)) return new Response("Too many requests", { status: 429 });
// Validation mélangée
const body = await req.json();
if (!isValid(body)) return new Response("Bad request", { status: 400 });
// Logging mélangé
console.log(`${req.method} ${req.url} by ${user.id}`);
// La logique métier enterrée tout en bas
return processBusinessLogic(body);
}
Chaque nouvelle préoccupation = modifier le handler. La logique transversale est emmêlée avec la logique métier.
La Solution
Chaque préoccupation est un middleware. Chaînez-les ensemble, chacun décide de traiter ou passer au suivant :
import { createChain } from "@pithos/core/eidos/chain/chain";
const handleRequest = createChain<Request, Response>(
// Auth : rejeter ou enrichir et passer
(req, next) => {
const user = verifyToken(req.headers.authorization);
if (!user) return new Response("Unauthorized", { status: 401 });
return next({ ...req, user });
},
// Rate limit : rejeter ou passer
(req, next) => {
if (isRateLimited(req.user.id)) return new Response("Too Many Requests", { status: 429 });
return next(req);
},
// Validation : rejeter ou passer
(req, next) => {
if (!isValid(req.body)) return new Response("Bad Request", { status: 400 });
return next(req);
},
// Logger : observer et toujours passer
(req, next) => {
console.log(`${req.method} ${req.url}`);
return next(req);
},
);
Ajoutez, supprimez ou réordonnez les middlewares sans toucher aux autres. Chaque handler a une responsabilité unique. Ça vous dit quelque chose ? C'est exactement comme ça que fonctionnent Express, Hono et Koa sous le capot.
Démo
- chain.ts
- Usage
/**
* HTTP Middleware using Chain of Responsibility pattern.
*
* createChain() works like Express/Hono/Koa middleware:
* each handler can either short-circuit (return a response) or pass to next.
*/
import { createChain, type Handler } from "@pithos/core/eidos/chain/chain";
import type { Request, Response, MiddlewareStep, MiddlewareConfig } from "./types";
interface MiddlewareDef {
key: keyof MiddlewareConfig;
name: string;
icon: string;
check: (req: Request) => { pass: true; req: Request; message: string } | { pass: false; response: Response; message: string };
}
const MIDDLEWARE_DEFS: MiddlewareDef[] = [
{
key: "rateLimit", name: "Rate Limit", icon: "⏱️",
check: (req) => {
const token = req.headers.authorization || "";
if (token.includes("rate-limited")) return { pass: false, response: { status: 429, statusText: "Too Many Requests", body: "Rate limit exceeded" }, message: "User exceeded rate limit" };
return { pass: true, req, message: "Within rate limit (42/100 requests)" };
},
},
{
key: "auth", name: "Auth", icon: "🔐",
check: (req) => {
const token = req.headers.authorization;
if (!token) return { pass: false, response: { status: 401, statusText: "Unauthorized", body: "Missing authorization token" }, message: "No token provided" };
return { pass: true, req: { ...req, user: { id: token.replace("Bearer ", "") } }, message: `Token verified: ${token.slice(0, 20)}...` };
},
},
{
key: "validation", name: "Validation", icon: "✅",
check: (req) => {
const body = req.body as Record<string, unknown> | undefined;
const isValid = body && typeof body.name === "string" && typeof body.email === "string";
if (!isValid) return { pass: false, response: { status: 400, statusText: "Bad Request", body: "Missing required fields: email" }, message: "Body validation failed" };
return { pass: true, req, message: "Body schema valid" };
},
},
{
key: "logging", name: "Logger", icon: "📝",
check: (req) => ({ pass: true, req, message: `${req.method} ${req.path}` }),
},
];
function toHandler(def: MiddlewareDef): Handler<Request, Response> {
return (req, next) => {
const result = def.check(req);
return result.pass ? next(result.req) : result.response;
};
}
export function buildPipeline(config: MiddlewareConfig): (req: Request) => Response {
const handlers = MIDDLEWARE_DEFS.filter((def) => config[def.key]).map(toHandler);
handlers.push(() => ({ status: 200, statusText: "OK", body: "User created successfully" }));
return createChain(...handlers);
}
export async function executeMiddlewareChainAnimated(
request: Request,
config: MiddlewareConfig,
onStep: (step: MiddlewareStep) => void,
): Promise<Response> {
let req = { ...request };
for (const def of MIDDLEWARE_DEFS) {
if (!config[def.key]) continue;
await new Promise<void>((r) => setTimeout(r, 400));
const result = def.check(req);
if (!result.pass) {
onStep({ name: def.name, icon: def.icon, passed: false, message: result.message, response: result.response });
return result.response;
}
req = result.req;
onStep({ name: def.name, icon: def.icon, passed: true, message: result.message });
}
await new Promise<void>((r) => setTimeout(r, 400));
const response = { status: 200, statusText: "OK", body: "User created successfully" };
onStep({ name: "Handler", icon: "🎯", passed: true, message: "Business logic executed", response });
return response;
}
import { useState, useCallback } from "react";
import { executeMiddlewareChainAnimated } from "@/lib/chain";
import { REQUEST_PRESETS } from "@/data/middleware";
import type { Request, Response, MiddlewareStep, MiddlewareConfig } from "@/lib/types";
export function useMiddlewareSimulator() {
const [selectedPreset, setSelectedPreset] = useState(0);
const [config, setConfig] = useState<MiddlewareConfig>({ auth: true, rateLimit: true, validation: true, logging: true });
const [steps, setSteps] = useState<MiddlewareStep[]>([]);
const [finalResponse, setFinalResponse] = useState<Response | null>(null);
const [isRunning, setIsRunning] = useState(false);
const handleReset = useCallback(() => {
setSteps([]);
setFinalResponse(null);
}, []);
const handleRun = useCallback(async () => {
setIsRunning(true);
setSteps([]);
setFinalResponse(null);
const request = REQUEST_PRESETS[selectedPreset].request as Request;
const response = await executeMiddlewareChainAnimated(request, config, (step) => {
setSteps((prev) => [...prev, step]);
});
setFinalResponse(response);
setIsRunning(false);
}, [selectedPreset, config]);
const toggleMiddleware = useCallback((key: keyof MiddlewareConfig) => {
setConfig((prev) => ({ ...prev, [key]: !prev[key] }));
setSteps([]);
setFinalResponse(null);
}, []);
const selectPreset = useCallback((i: number) => {
setSelectedPreset(i);
setSteps([]);
setFinalResponse(null);
}, []);
return {
selectedPreset, config, steps, finalResponse, isRunning,
handleRun, handleReset, toggleMiddleware, selectPreset,
currentRequest: REQUEST_PRESETS[selectedPreset],
};
}
Analogie
La sécurité à l'aéroport. Votre carte d'embarquement est vérifiée (auth), votre sac passe aux rayons X (validation), vous passez le détecteur de métaux (screening), puis vous êtes autorisé à embarquer. Chaque point de contrôle peut vous arrêter ou vous laisser passer au suivant. Ajouter un nouveau contrôle ne change pas les autres.
Quand l'Utiliser
- Plusieurs handlers peuvent traiter une requête
- Le handler n'est pas connu à l'avance
- Vous voulez ajouter/supprimer des étapes de traitement dynamiquement
- Construction de pipelines middleware (comme Express, Hono, Koa)
Quand NE PAS l'Utiliser
Si votre traitement est toujours la même séquence fixe sans sorties anticipées, une simple composition de fonctions ou un pipe est plus clair. Chain brille quand les handlers peuvent court-circuiter.
API
- createChain — Construire une chaîne de handlers avec input/output typés
- safeChain — Chaîne qui capture les erreurs et retourne un Result