Chain of Responsibility Pattern
Pass a request along a chain of handlers. Each handler decides to process the request or pass it to the next handler.
The Problemβ
You're building an API. Every request needs authentication, validation, rate limiting, and logging. But it's all tangled together:
async function handleRequest(req: Request): Promise<Response> {
// Auth mixed in
const user = verifyToken(req.headers.authorization);
if (!user) return new Response("Unauthorized", { status: 401 });
// Rate limiting mixed in
if (isRateLimited(user.id)) return new Response("Too many requests", { status: 429 });
// Validation mixed in
const body = await req.json();
if (!isValid(body)) return new Response("Bad request", { status: 400 });
// Logging mixed in
console.log(`${req.method} ${req.url} by ${user.id}`);
// Actual business logic buried at the bottom
return processBusinessLogic(body);
}
Every new concern = modify the handler. Cross-cutting logic is tangled with business logic.
The Solutionβ
Each concern is a middleware. Chain them together, each one decides to handle or pass to next:
import { createChain } from "@pithos/core/eidos/chain/chain";
const handleRequest = createChain<Request, Response>(
// Auth: reject or enrich and pass
(req, next) => {
const user = verifyToken(req.headers.authorization);
if (!user) return new Response("Unauthorized", { status: 401 });
return next({ ...req, user });
},
// Rate limit: reject or pass
(req, next) => {
if (isRateLimited(req.user.id)) return new Response("Too Many Requests", { status: 429 });
return next(req);
},
// Validation: reject or pass
(req, next) => {
if (!isValid(req.body)) return new Response("Bad Request", { status: 400 });
return next(req);
},
// Logger: observe and always pass
(req, next) => {
console.log(`${req.method} ${req.url}`);
return next(req);
},
);
Add, remove, or reorder middleware without touching others. Each handler has a single responsibility. Sound familiar? It's exactly how Express, Hono, and Koa work under the hood.
Live Demoβ
- 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],
};
}
Real-World Analogyβ
Airport security. Your boarding pass is checked (auth), your bag goes through X-ray (validation), you walk through the metal detector (screening), then you're cleared to board. Each checkpoint can stop you or let you through to the next. Adding a new check doesn't change the others.
When to Use Itβ
- Multiple handlers might process a request
- The handler isn't known in advance
- You want to add/remove processing steps dynamically
- Building middleware pipelines (like Express, Hono, Koa)
When NOT to Use Itβ
If your processing is always the same fixed sequence with no early exits, a simple function composition or pipe is clearer. Chain shines when handlers can short-circuit.
APIβ
- createChain β Build a handler chain with typed input/output
- safeChain β Chain that catches errors and returns Result