Middleware
Middleware are functions chained around your handlers. Watch the onion model run code before and after next(), see an error skip straight to error middleware, and learn how Nest splits the idea into guards, interceptors and pipes.
🎯 What you'll learn
- What middleware is
- The chain: order is everything
- The onion: code before and after next()
- Error middleware, scoping, and Nest's specialized variants
📚 Before you start
1What Middleware Is
Middleware is a function that sits between the server receiving a request and your route handler running. Each middleware gets the request, can read or modify it, and then either passes control on (next()) or ends the request itself by sending a response.
// Express signature: (req, res, next)
function requestTimer(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on("finish", () => console.log(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`));
next(); // hand off to the next middleware / the route handler
}
app.use(requestTimer);Middleware is how cross-cutting concerns — things every (or most) routes need — are written once instead of copy-pasted into every handler: logging, CORS, body parsing, authentication (12-authentication_authorization/), rate limiting, compression, request IDs.
2The Chain: Order Is Everything
Middleware runs in registration order. Each one decides whether the request continues:
app.use(express.json()); // 1. parse body -> req.body
app.use(requestId); // 2. attach req.id
app.use(requestTimer); // 3. start timing
app.use("/admin", requireAdmin); // 4. only for paths under /admin
app.get("/admin/stats", statsHandler);- A middleware that calls
next()passes control down the chain. - A middleware that sends a response (
res.status(401).json(...)) and does not callnext()short-circuits — nothing after it runs. - A middleware that does neither leaves the request hanging until the client times out.
Because order matters, putting requireAdmin before express.json() means it can't see the body; putting a logger after the router means it never sees requests the router already answered.
3The "Onion": Code Before and After next()
When next() returns (or its promise resolves, in Koa/Nest/async-style frameworks), the downstream handler has finished. Code after next() therefore runs on the way out:
// Koa-style async middleware
app.use(async (ctx, next) => {
const start = Date.now(); // inbound: before the handler
await next(); // everything downstream runs here
ctx.set("X-Response-Time", `${Date.now() - start}ms`); // outbound: after the handler
});This gives every middleware two hooks — before and after — around everything nested inside it, like the layers of an onion. Express's classic callback next() doesn't return a promise you can await, which is why Express code uses res.on("finish") for "after" logic instead.
4Error-Handling Middleware
Express recognizes middleware with four parameters as an error handler. Calling next(err) (or throwing, in Express 5 async handlers) skips all regular middleware and jumps to the next error handler:
app.get("/orders/:id", async (req, res, next) => {
try {
res.json(await ordersService.findById(req.params.id));
} catch (err) {
next(err); // skip to error-handling middleware
}
});
// registered LAST, after all routes
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
res.status(500).json({ error: "internal error" });
});11-exception_handling/ builds the full mapping of error types to status codes on top of this.
5Scoping: Global, Path, Router, Route
app.use(cors()); // global: every request
app.use("/api", apiRateLimiter); // path-scoped: /api/**
const admin = express.Router();
admin.use(requireAdmin); // router-scoped: only routes on this router
admin.get("/stats", statsHandler);
app.use("/admin", admin);
app.post("/upload", multerSingle("file"), uploadHandler); // route-level: just this routeApply middleware at the narrowest scope that's correct: an expensive multipart parser belongs on the upload route only, not globally.
6NestJS: Middleware vs Guards vs Interceptors vs Pipes
Nest splits Express's single "middleware" idea into specialized stages that run in a fixed order:
| Stage | Runs | Typical job | Knows the target handler? |
|---|---|---|---|
| Middleware | first | raw req/res work: logging, CORS, request IDs | no |
| Guards | after middleware | may this request proceed? (auth, roles) | yes (ExecutionContext) |
| Interceptors (before) | after guards | timing, caching, transforming input | yes |
| Pipes | right before the handler | validate/transform params (08-validation_and_dtos/) | yes |
| Handler | — | the controller method | — |
| Interceptors (after) | after the handler | map/wrap the response | yes |
| Exception filters | on throw | turn errors into responses | yes |
The practical difference: a guard knows which controller method is about to run (and can read its @Roles() metadata); plain middleware doesn't. 07-request_response_lifecycle/ walks a request through this whole pipeline.
Under the HoodHow next Is Implemented▾
Express keeps an array of layers (each with a path matcher and a handler). Dispatch is a tiny recursive function closed over an index:
function handle(req, res, stack, done) {
let idx = 0;
function next(err?) {
const layer = stack[idx++];
if (!layer) return done(err); // ran off the end -> 404 / final handler
if (!layer.matches(req.path)) return next(err); // not for this path, skip
const isErrorHandler = layer.handle.length === 4; // arity check!
if (err && !isErrorHandler) return next(err); // in error mode, skip normal middleware
if (!err && isErrorHandler) return next(); // in normal mode, skip error handlers
try {
err ? layer.handle(err, req, res, next) : layer.handle(req, res, next);
} catch (e) {
next(e); // sync throw -> error mode
}
}
next();
}Three consequences fall straight out of this:
- Arity matters. An error handler written
(err, req, res) => ...(3 params) is not treated as an error handler, because Express checksfn.length === 4. - "Calling next twice" is a real bug. Each call advances
idx— a middleware that callsnext()and then also sends a response will run the rest of the chain and respond, causing "Cannot set headers after they are sent." - Composition is just function nesting. Koa's
koa-composedoes the same withdispatch(i) => Promise.resolve(fn(ctx, () => dispatch(i + 1))), which is whyawait next()naturally gives you the onion.
⚠️ Common Pitfalls
- Forgetting to call
next()(and not responding) — the request hangs until timeout. - Calling
next()and also sending a response — "Cannot set headers after they are sent to the client." - Wrong order — auth after the route, body parser after a middleware that reads the body, error handler registered before routes.
- Error handler with 3 parameters — silently treated as normal middleware.
- Unhandled promise rejections in Express 4 async middleware — a rejected promise is not routed to
next(err)automatically; wrap handlers or upgrade to Express 5. - Global heavy middleware — parsing multipart or decompressing bodies on every route wastes CPU; scope it.
📋 Quick Reference
| Need | Do |
|---|---|
| run for every request | app.use(fn) |
| run for a path prefix | app.use("/api", fn) |
| run for one route | app.get("/x", fn, handler) |
| continue | next() |
| stop with a response | res.status(...).json(...) and don't call next() |
| raise an error | next(err) |
| handle errors | app.use((err, req, res, next) => ...) — 4 params, registered last |
| "after" logic | res.on("finish", ...) (Express) / code after await next() (Koa/Nest) |
🧠 Key takeaways
- Middleware =
(req, res, next)functions run in registration order. - Each either ends the response or calls
next(); code afterawait next()runs on the way back (onion). - Error middleware
(err, req, res, next)goes last; thrown errors skip to it. - Scope it: global, path, router or route. Nest splits concerns into guards, interceptors, pipes, filters.
🎤 Interview Q&A
Q1What is middleware?core▾
Components in the request pipeline that run before and/or after the endpoint, each able to inspect/modify the request and response, short-circuit, or pass control on. Used for logging, auth, CORS, compression, error handling, rate limiting.
- Pipeline analogy? Russian dolls: each wraps the next.
Q2Why does middleware order matter?core▾
Each middleware only sees what earlier ones did: error handling must wrap everything, authentication must run before authorization, CORS before auth for preflights, static files before routing to short-circuit.
- ASP.NET Core typical order? Exception handler → HSTS/HTTPS → static files → routing → CORS → authentication → authorization → endpoints.
Q3Middleware vs filters/interceptors?core▾
Middleware is framework-level and route-agnostic; filters/interceptors/guards run inside the endpoint pipeline with access to route and action metadata (e.g. which controller, attributes).
- When use a filter? Logic that depends on the specific action, like a validation or authorization attribute.
Q4How do you write middleware that measures request duration?senior▾
Record a high-resolution start time, call the next handler, then on completion (res.on('finish') in Node, after await next() in ASP.NET) compute duration and record a metric with route template, method and status.
- Why route template, not raw path? Raw paths with IDs explode metric cardinality.
Q5What's a common middleware bug?core▾
Forgetting to call next() (requests hang), calling it twice, writing headers after the response started, or swallowing errors instead of passing them to the error handler.
- Express error signature? Four arguments:
(err, req, res, next).
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.