// ───────────────────────────────────────────────────────────────────────────── // CHAIN OF RESPONSIBILITY [Behavioral] // ───────────────────────────────────────────────────────────────────────────── // INTENT : Avoid coupling the sender of a request to its receiver by giving more than one object a chance // to handle it; chain the receivers and pass the request along until one handles it. // PROBLEM : Rate limiting, auth, validation, logging — each should be a self-contained step that can handle // the request, stop everything, or pass it on, without one giant if/else in the route. // SAY : Participants: Handler (interface: setNext(), handle()), ConcreteHandler (handles or forwards), // Client (assembles the chain, fires the request). Pitch: "a pipeline where each link decides: // handle, pass along, or stop." // SEEN IN : Express / Koa / Fastify middleware (`(req, res, next)`); axios & fetch interceptors; Redux // middleware (`store => next => action`); DOM event bubbling + `stopPropagation()`; try/catch // propagating up the call stack; Webpack loader chains; logger level filters. // VS : Decorator ALWAYS forwards and wraps behavior around the call; CoR links may STOP the chain and // typically one (or none) handles. Composite is a tree that fans out; CoR is linear. Command is // the request object itself; CoR is who gets to process it. // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── Classic shape (classes, so the participant names are visible) class Handler { // Handler — default behavior: forward #next = null; setNext(handler) { this.#next = handler; return handler; } // returns next so chains read left→right handle(req, res) { return this.#next ? this.#next.handle(req, res) : res; } } class RateLimitHandler extends Handler { // ConcreteHandler — may stop the chain constructor(limit) { super(); this.limit = limit; this.seen = new Map(); } handle(req, res) { const n = (this.seen.get(req.ip) ?? 0) + 1; this.seen.set(req.ip, n); if (n > this.limit) return { ...res, status: 429, body: 'too many requests' }; return super.handle(req, res); } } class AuthHandler extends Handler { // ConcreteHandler — enriches and forwards handle(req, res) { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) return { ...res, status: 401, body: 'unauthorized' }; return super.handle({ ...req, user: { id: token } }, res); } } class ValidateHandler extends Handler { // ConcreteHandler handle(req, res) { if (req.method === 'POST' && typeof req.body?.sku !== 'string') return { ...res, status: 400, body: 'sku required' }; return super.handle(req, res); } } class RouteHandler extends Handler { // terminal ConcreteHandler handle(req, res) { return { ...res, status: 201, body: `order ${req.body.sku} for ${req.user.id}` }; } } const chain = new RateLimitHandler(2); chain.setNext(new AuthHandler()).setNext(new ValidateHandler()).setNext(new RouteHandler()); const base = { ip: '10.0.0.1', method: 'POST', headers: { authorization: 'Bearer u7' }, body: { sku: 'A1' } }; const res0 = { status: 200, body: '' }; assert.deepEqual(chain.handle(base, res0), { status: 201, body: 'order A1 for u7' }); // all links passed assert.deepEqual(chain.handle({ ...base, headers: {} }, res0), { status: 401, body: 'unauthorized' }); // stopped at auth assert.deepEqual(chain.handle({ ...base, body: {} }, res0), { status: 429, body: 'too many requests' }); // 3rd hit from same ip assert.deepEqual(chain.handle({ ...base, ip: '10.0.0.2', body: {} }, res0), { status: 400, body: 'sku required' }); assert.deepEqual(new Handler().handle(base, res0), res0); // empty chain: unchanged // ── Idiomatic JS shape: Express/Koa-style middleware array with next() const compose = (middlewares) => (ctx) => { let i = -1; const dispatch = (idx) => { if (idx <= i) throw new Error('next() called multiple times'); i = idx; const mw = middlewares[idx]; return mw ? mw(ctx, () => dispatch(idx + 1)) : ctx; }; return dispatch(0); }; const trace = []; const logger = (ctx, next) => { trace.push(`→ ${ctx.path}`); const out = next(); trace.push(`← ${ctx.status}`); return out; }; const auth = (ctx, next) => { if (!ctx.token) { ctx.status = 401; return ctx; } ctx.user = ctx.token; return next(); }; const apiOnly = (ctx, next) => (ctx.path.startsWith('/api') ? next() : Object.assign(ctx, { status: 404 })); const handler = (ctx) => Object.assign(ctx, { status: 200, body: `hello ${ctx.user}` }); const app = compose([logger, auth, apiOnly, handler]); const okCtx = app({ path: '/api/me', token: 'ada', status: 0 }); assert.equal(okCtx.status, 200); assert.equal(okCtx.body, 'hello ada'); assert.deepEqual(trace, ['→ /api/me', '← 200']); // onion: logger sees the final status on the way out trace.length = 0; assert.equal(app({ path: '/api/me', status: 0 }).status, 401); // auth short-circuited; handler never ran assert.deepEqual(trace, ['→ /api/me', '← 401']); assert.equal(app({ path: '/nope', token: 'x', status: 0 }).status, 404); const doubleNext = compose([(ctx, next) => { next(); return next(); }, (ctx) => ctx]); assert.throws(() => doubleNext({}), /next\(\) called multiple times/); // the classic Koa guard console.log('ok: design-patterns/behavioral/chain-of-responsibility.js');