Chain of Responsibility

Advanced · 11 min read · ▶ live playground · ✦ checkpoint

An API endpoint checks authentication, then rate limits, then validates input. The fourth check turns one readable guard into another nested branch, and changing the order means editing the same central function again.

The Chain of Responsibility pattern passes a request through an ordered chain of handlers until one handles it or the chain ends. It makes steps addable and reorderable, but the final receiver and the path a request took can become hard to see.

Before: one function owns every decision

The endpoint decides every rejection and the successful response:

interface ApiRequest {
  user: string | null;
  remainingRequests: number;
  body: string;
}
 
function handleRequest(request: ApiRequest): string {
  if (request.user === null) {
    return "401: sign in required";
  } else if (request.remainingRequests <= 0) {
    return "429: rate limit exceeded";
  } else if (request.body.trim() === "") {
    return "400: body required";
  } else {
    return `200: hello ${request.user}`;
  }
}

Adding an account-suspension check edits handleRequest(). Reusing authentication on a different endpoint copies a branch. Moving rate limiting before authentication changes this function again and can alter both cost and what information an anonymous caller learns.

Small guards are not inherently wrong. The smell appears when the sequence varies, steps are reused across pipelines, or one function becomes the registry for every request policy.

After: each handler chooses to stop or forward

A handler is an object that receives a request and either produces a result or forwards the request. A chain is the ordered set of linked handlers through which that request can travel. To short-circuit is to return a result without forwarding to the next link.

The shared Handler contract exposes setNext() for assembly and handle() for processing. BaseHandler stores the next link, while concrete handlers own one decision each.

class diagram

API checks form a linked chain of handlers

API checks form a linked chain of handlers
  1. 1Handler gives every link the same assembly and request operations.
  2. 2HandlerComponent makes conformance explicit before the next-link ownership appears.
  3. 3BaseHandler inherits that contract and provides the shared forwarding behavior.
  4. 4The base owns the next Handler and defines what happens when no next link exists.
  5. 5AuthHandler rejects an anonymous request or calls next(request).
  6. 6RateLimitHandler can stop an exhausted request or forward an allowed one.
  7. 7ApiHandler handles requests that survive the earlier checks and ends this chain.

HandlerComponent is a thin abstract conformance base, not another pattern role. It lets the diagram show implements on one edge and the stored next Handler on another. Pointing both relationships from BaseHandler to Handler would create duplicate directed edges and blur two different facts.

The code follows the same split:

interface ApiRequest {
  user: string | null;
  remainingRequests: number;
}
 
interface Handler {
  setNext(handler: Handler): Handler;
  handle(request: ApiRequest): string;
}
 
abstract class HandlerComponent implements Handler {
  public abstract setNext(handler: Handler): Handler;
  public abstract handle(request: ApiRequest): string;
}
 
abstract class BaseHandler extends HandlerComponent {
  private nextHandler: Handler | null = null;
 
  public setNext(handler: Handler): Handler {
    this.nextHandler = handler;
    return handler;
  }
 
  protected next(request: ApiRequest): string {
    return this.nextHandler?.handle(request) ?? "404: unhandled request";
  }
}
 
class AuthHandler extends BaseHandler {
  public handle(request: ApiRequest): string {
    if (request.user === null) {
      return "401: sign in required";
    }
 
    return this.next(request);
  }
}
 
class RateLimitHandler extends BaseHandler {
  public handle(request: ApiRequest): string {
    if (request.remainingRequests <= 0) {
      return "429: rate limit exceeded";
    }
 
    return this.next(request);
  }
}
 
class ApiHandler extends BaseHandler {
  public handle(request: ApiRequest): string {
    return `200: hello ${request.user}`;
  }
}
 
const auth = new AuthHandler();
const rateLimit = new RateLimitHandler();
const endpoint = new ApiHandler();
 
auth.setNext(rateLimit).setNext(endpoint);
 
console.log(auth.handle({ user: null, remainingRequests: 3 }));
console.log(auth.handle({ user: "Ada", remainingRequests: 0 }));
console.log(auth.handle({ user: "Ada", remainingRequests: 2 }));

401: sign in required 429: rate limit exceeded 200: hello Ada

The anonymous request stops at AuthHandler. Ada's exhausted request reaches RateLimitHandler and stops there. The allowed request crosses both checks and ends at ApiHandler.

Returning the assigned handler from setNext() makes construction read in chain order. The request still enters through auth, the head of the chain. A container or setup function should build that order once rather than relinking handlers during a request.

Middleware is a chain of responsibility

The familiar Express/Next.js middleware shape—(req, res, next) => ...—is a Chain of Responsibility. One middleware can send a response and stop, or call next() to pass control to the next middleware. Authentication, rate limiting, logging, and validation form the same ordered pipeline as these handler objects.

The callback is another representation of the next link. In the class version, this.next(request) forwards to a stored Handler; in middleware, next() invokes framework-managed continuation. Both designs let one step short-circuit the rest.

Some middleware creates an onion-shaped pipeline, where each step runs work before and after the later steps reached by next(). That is still a chain, but error propagation and response timing become part of its contract.

When NOT to use it

A request can fall off the end unhandled. Decide that behavior deliberately: return a default response, throw a specific error, require a terminal handler, or represent “not handled” in the return type. This example returns 404: unhandled request from BaseHandler; silently returning undefined would hide a broken chain.

Long chains are hard to trace because the call site shows only the head. Which handler stopped a request may depend on runtime configuration and order. Add request tracing, stable handler names, and focused pipeline tests when a chain grows.

For three fixed guards used by one endpoint, sequential if statements can be clearer. Chain of Responsibility trades a visible central flow for reusable, reorderable links and possible short-circuiting. Use it when those links really vary; do not turn every validation line into a class.

Checkpoint

Answer all three to mark this lesson complete

Chain of Responsibility spreads one request across replaceable handlers. Next, Visitor moves replaceable operations across a stable family of element types.

+50 XP on completion