Middleware Chain#
The middleware pattern composes a request handler from a stack of wrapping functions. Each middleware sees the request, may modify it, calls the next, then sees the response.
flowchart LR
Req([Request]) --> M1[Auth] --> M2[Logging] --> M3[Rate limit] --> M4[Tracing] --> H[Handler]
H --> M4 --> M3 --> M2 --> M1 --> Resp([Response])
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
class M1,M2,M3,M4 edge;
class H service;
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
Found in Express, Koa, FastAPI, ASP.NET Core, Rails Rack, Gin, Spring filters, gRPC interceptors. The same shape is called interceptor in some stacks and decorator in others.
A middleware chain wraps a core handler in an onion of cross-cutting concerns: auth, logging, tracing, retries, compression, CORS, error handling. Each layer runs before and after the layer it wraps.
The onion model#
flowchart TB
subgraph L1[Auth]
subgraph L2[Logging]
subgraph L3[Rate limit]
subgraph L4[Tracing]
H[Handler]
end
end
end
end
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
A request enters the outermost layer and travels inward to the handler; the response travels outward through the same layers in reverse.
Order matters#
// Express
app.use(logger); // 1
app.use(auth); // 2
app.use(rateLimit); // 3
app.get("/me", handler); // 4
- Logger sees every request including pre-auth ones.
- Auth must come before any code that depends on
req.user. - Rate limit can be before or after auth depending on whether limits are per-IP or per-user.
A mis-ordered chain is a real source of security bugs (auth after a handler that already responded).
Implementations#
Function-composition style (Express, Koa, Connect)#
function logger(req, res, next) {
console.log(req.method, req.url);
next(); // call the next middleware
}
function auth(req, res, next) {
const user = verifyJwt(req.headers.authorization);
if (!user) return res.status(401).end();
req.user = user;
next();
}
next() is the explicit continuation. Forgetting to call it hangs the request.
Decorator / class style (ASP.NET Core, Spring)#
public class LoggingMiddleware {
private readonly RequestDelegate _next;
public LoggingMiddleware(RequestDelegate next) => _next = next;
public async Task Invoke(HttpContext ctx) {
log(ctx);
await _next(ctx);
logResponse(ctx);
}
}
The framework wires _next automatically; you opt in by calling it.
Interceptor style (gRPC, Java filters)#
A single intercept(call, next) method that both sides of the conversation flow through. Same semantics, different name.
Short-circuiting#
Any middleware can refuse to call next() and respond directly:
function maintenance(req, res, next) {
if (process.env.MAINTENANCE) return res.status(503).send("down");
next();
}
Useful for: auth failures, rate limit hits, maintenance mode, request validation, conditional routing.
Async middleware#
# FastAPI / Starlette
@app.middleware("http")
async def add_request_id(request, call_next):
rid = request.headers.get("x-request-id") or str(uuid4())
request.state.request_id = rid
response = await call_next(request)
response.headers["x-request-id"] = rid
return response
Modern frameworks handle async middleware natively; the chain awaits each next() call.
Error handling#
Errors must propagate through the chain so error middleware can render them:
// Express - error-handling middleware has 4 params
app.use((err, req, res, next) => {
log.error(err);
res.status(err.status || 500).send({ error: err.message });
});
Pitfall: throwing inside a setTimeout or unawaited promise bypasses the chain. Wrap async handlers with a try/catch that calls next(err).
Common middleware#
| Concern | Where |
|---|---|
| Request id | First |
| Logging | After request id, before auth |
| Compression | Outermost (after rendering) |
| Auth | Before any handler |
| Rate limit | Before / after auth depending on policy |
| Tracing | After request id |
| Error handler | Last (catches everything) |
| Health check | Skip rest of chain |
Where middleware sits#
flowchart TB
MW((Middleware<br/>chain))
BPL[Behavioral patterns<br/>Chain-of-Responsibility]
SP[Structural patterns<br/>Decorator analog]
DI[Dependency injection<br/>how middleware gets services]
GW[API Gateway<br/>same chain at the edge]
BPL --> MW
SP -. analog .- MW
DI --> MW
GW -. fleet-scale chain .- MW
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class MW service;
class BPL,SP,DI,GW datastore;
Glossary & fundamentals#
| Tag | Concept | What it is | Page |
|---|---|---|---|
LLD |
Behavioral patterns | Chain of Responsibility is the parent | behavioral-patterns |
LLD |
Structural patterns | Decorator pattern is closely related | structural-patterns |
LLD |
Dependency injection | how middleware acquires services | dependency-injection |
HLD |
API gateway | a chain at the network edge | api-gateway |
Quick reference#
Shape#
Onion of wrappers around a handler; each wrapper sees req on way in and res on way out.
Conventions#
- Request id: first
- Logging: after id, before auth
- Auth: before any handler
- Rate limit: before/after auth depending on policy
- Compression: outermost
- Error handler: last
Short-circuit#
Don't call next() → respond immediately. Used for: 401, 429, maintenance, health check.
Async#
Modern frameworks await next(); wrap async work in try/catch and call next(err).
Framework names#
| Framework | Term |
|---|---|
| Express, Koa | Middleware |
| ASP.NET Core | Middleware |
| FastAPI / Starlette | Middleware |
| Spring | Filter / Interceptor |
| gRPC | Interceptor |
| Rails | Rack middleware |
| Java Servlets | Filter |
Pitfalls#
- Forgetting
next()→ request hangs - Errors thrown in
setTimeoutbypass chain - Misordered chain (auth after handler) = security bug
- Reading body twice (consume in one middleware, others get empty)
Refs#
- Express docs
- ASP.NET Core middleware
- FastAPI middleware
FAQ#
What is a middleware in web frameworks?#
Middleware is a function that sees a request, can modify it, calls the next layer, then sees the response. Chained together they form a pipeline of cross-cutting behaviour.
What is the difference between middleware and an interceptor?#
They are the same idea under different names. Express and Koa call them middleware; gRPC and Spring call them interceptors; the onion model is identical.
What order should middleware run in?#
Outermost first for setup (logging, tracing, request ID), then auth, then rate limit, then business handlers. The reverse order runs on the way out for the response.
How does middleware handle errors?#
An error middleware sits at one end of the chain. When an inner layer throws or calls next with an error, the error middleware translates it into a clean HTTP response.
Is middleware the same as a filter or aspect?#
Conceptually yes. Java servlet filters and AOP aspects achieve the same goal of wrapping a handler with reusable behaviour using slightly different APIs.
Related Topics#
- Behavioral Patterns: Chain of Responsibility is the underlying pattern
- Structural Patterns: each middleware is essentially a Decorator
- API Gateway: the same chain pattern at the network edge
Further reading#
- Doc - Express - Using middleware
- Doc - ASP.NET Core - Middleware
- Doc - FastAPI - Middleware
- Blog - TJ Holowaychuk - Middleware in Express