Engineering Notes
Home/Backend/Frameworks/Middleware
Backend · Frameworks

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.

⚙️ Backend● Intermediate⏱ 6 min read▶ 1 interactive🛠️ 3 exercises🎤 5 interview Q&As

🎯 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.

ts
// 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:

ts
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 call next() 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:

ts
// 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:

ts
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

ts
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 route

Apply 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:

StageRunsTypical jobKnows the target handler?
Middlewarefirstraw req/res work: logging, CORS, request IDsno
Guardsafter middlewaremay this request proceed? (auth, roles)yes (ExecutionContext)
Interceptors (before)after guardstiming, caching, transforming inputyes
Pipesright before the handlervalidate/transform params (08-validation_and_dtos/)yes
Handler—the controller method—
Interceptors (after)after the handlermap/wrap the responseyes
Exception filterson throwturn errors into responsesyes

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:

ts
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:

  1. Arity matters. An error handler written (err, req, res) => ... (3 params) is not treated as an error handler, because Express checks fn.length === 4.
  2. "Calling next twice" is a real bug. Each call advances idx — a middleware that calls next() and then also sends a response will run the rest of the chain and respond, causing "Cannot set headers after they are sent."
  3. Composition is just function nesting. Koa's koa-compose does the same with dispatch(i) => Promise.resolve(fn(ctx, () => dispatch(i + 1))), which is why await next() naturally gives you the onion.
A request flows inward through each middleware's before-next code to the handler, then back outward through each middleware's after-next code; a middleware that responds without calling next short-circuits the rest
A request flows inward through each middleware's before-next code to the handler, then back outward through each middleware's after-next code; a middleware that responds without calling next short-circuits the rest

⚠️ Common Pitfalls

  1. Forgetting to call next() (and not responding) — the request hangs until timeout.
  2. Calling next() and also sending a response — "Cannot set headers after they are sent to the client."
  3. Wrong order — auth after the route, body parser after a middleware that reads the body, error handler registered before routes.
  4. Error handler with 3 parameters — silently treated as normal middleware.
  5. 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.
  6. Global heavy middleware — parsing multipart or decompressing bodies on every route wastes CPU; scope it.

📋 Quick Reference

NeedDo
run for every requestapp.use(fn)
run for a path prefixapp.use("/api", fn)
run for one routeapp.get("/x", fn, handler)
continuenext()
stop with a responseres.status(...).json(...) and don't call next()
raise an errornext(err)
handle errorsapp.use((err, req, res, next) => ...) — 4 params, registered last
"after" logicres.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 after await 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

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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).

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

1. Auth middleware registered after the routes…
Order is everything.
2. Where can you measure total handler time in one middleware?
Wrap around.
3. A middleware neither calls next() nor responds. Result?
Always do one or the other.
4. Nest's equivalent for 'may this request proceed?'
Authorization gate.