Middleware

Small functions that run before a request reaches its final route handler, each able to inspect, modify, or stop the request.

What is it?

Lots of behavior needs to happen on every (or almost every) request, regardless of which specific route it's headed for: logging that a request came in, checking whether the user is logged in, parsing the raw body of the request into usable data. Repeating that logic inside every single route handler would be tedious and error-prone.

Middleware are functions that sit in between the request arriving and the route handler that ultimately deals with it. Each middleware function gets a chance to look at the request, do some work, and then either pass control on to the next thing in line, or stop the chain early (for example, rejecting an unauthenticated request before it ever reaches the route).

Explain like I'm 10

Think of airport security checkpoints between the entrance and your gate: one checks your ticket, another scans your bag, another checks your ID. Each checkpoint either waves you through to the next one, or stops you right there if something's wrong. Your route handler is the gate — middleware is everything you pass through to get there.

Examples

A simple logging middleware

function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next(); // pass control to the next function in line
}

app.use(logger);

app.get("/users", (req, res) => {
  res.json(allUsers);
});

logger runs before every single route in this app, since it's registered with app.use() rather than tied to one specific path.

Middleware that can stop the chain

function requireAuth(req, res, next) {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: "Not authenticated" });
  }
  next();
}

app.get("/orders", requireAuth, (req, res) => {
  res.json(getOrdersFor(req.user));
});

requireAuth is attached to just this one route. If the check fails, it sends a response and never calls next(), so the route handler never runs at all.

How it works

Middleware functions run in the exact order they're registered, forming a chain. Each one receives (req, res, next) — the same request and response objects as a route handler, plus a next function. Calling next() moves on to the next middleware (or, at the end of the chain, the matching route handler). Not calling next() — for example, because you called res.send() instead — stops the chain right there, so nothing further down the line ever runs for that request.

Request
   │
   ▼
[logger]  → calls next()
   │
   ▼
[requireAuth]  → calls next() OR sends 401 and stops
   │
   ▼
[route handler]  → sends the final response

Why does it exist?

Middleware exists so that cross-cutting behavior — logging, auth checks, parsing request bodies, compressing responses — can be written once and applied to many routes, instead of being copy-pasted into every route handler individually.

When to use it

Use middleware for anything that needs to happen consistently across multiple routes: authentication checks, request logging, parsing incoming JSON bodies, or rejecting malformed requests before they reach your business logic.

When not to use it

Logic that's truly specific to one single route — and unlikely to ever be reused elsewhere — is usually simpler to just write directly inside that route's handler rather than extracting it into a separate middleware function.

Common mistakes

  • Forgetting to call next(), which leaves the request hanging with no response ever sent.

  • Calling next() after also sending a response, which can cause confusing "headers already sent" errors.

  • Registering middleware in the wrong order — for example, putting a route before the auth-check middleware that's supposed to protect it.

Practice exercises

  1. Easy:

    Write a middleware function that logs the current timestamp for every incoming request.

  2. Medium:

    Write a middleware function that rejects any request missing a x-api-key header with a 401 response.

  3. Hard:

    Explain what would happen if a middleware function neither calls next() nor sends a response, and why that's a bug.

Interview questions

What is middleware in a web framework?

A function that runs between an incoming request and its final route handler, able to inspect, modify, or halt the request before it continues down the chain.

What does calling `next()` do?

It passes control to the next middleware function in the chain, or to the matching route handler if there are no more middleware functions left.

Give an example of something commonly implemented as middleware.

Authentication checks, request logging, and parsing an incoming request body are all classic examples of middleware.

What three parameters does an Express middleware function typically receive, and what is each one for?

req (the incoming request, to read from), res (the outgoing response, to write to or send), and next (a function to call to hand control to whatever comes after this middleware in the chain).

What happens to a request if a middleware function never calls `next()` and never sends a response?

The request hangs indefinitely — nothing further in the chain ever runs, and since no response is ever sent, the client is left waiting until it eventually times out.

Why does forgetting to call `next()` "hang" the request instead of causing an immediate visible error?

Express has no way to know a middleware function is "finished" other than the function itself calling next() or sending a response — if neither happens, Express simply keeps waiting, since silently doing nothing is indistinguishable from still working on something slow.

Given `app.use(logger)` registered before `app.get("/users", handler)`, in what order do `logger` and `handler` run for a request to `/users`, and why?

logger runs first, then handler — middleware registered with app.use() runs for every matching request before the framework moves on to the matched route, and logger must call next() for handler to run at all.

What's the difference between middleware registered with `app.use(middleware)` and middleware attached to just one route, like `app.get("/orders", requireAuth, handler)`?

app.use(middleware) runs for every request that reaches it, regardless of path, while middleware listed as an extra argument to a specific route only runs for requests to that one route.

In `requireAuth`, if `req.headers.authorization` is missing, the function calls `res.status(401).json(...)` without calling `next()`. Does the route handler run?

No — because next() is never called, the chain stops at requireAuth; the route handler after it never executes for that request, so only the 401 response is sent.

Why is `return res.status(401).json(...)` written with a `return` in front of it, rather than calling it on its own line?

The return stops the function from executing any code after that line — without it, execution would continue past the response call and could reach a next() call further down, calling both next() and sending a response for the same request, which causes errors.

What would you expect if a middleware function called both `next()` and `res.send()` for the same request?

Whichever runs first sends the response for real, but calling res.send() a second time later in the chain (since next() let the chain continue) would throw a "headers already sent" error, because a single request can only be answered once.

Why does the order in which middleware is registered matter, using an auth-check-before-route example?

Middleware runs strictly in registration order, so an auth check must be registered before the route it's meant to protect — if it were registered after, the route handler would already have run and responded before the auth check ever got a chance to block it.

A developer registers `app.get("/admin", handler)` before the `requireAuth` middleware meant to protect it. What's the consequence?

requireAuth never runs for requests to /admin, since the route already matched and its handler already ran and responded — the protection is effectively bypassed entirely, regardless of what requireAuth checks.

Why is repeating an authentication check inside every route handler worse practice than extracting it into one middleware function?

Duplicating the check means every route must remember to include it correctly, and a single missed or subtly-wrong copy creates a security hole; a shared middleware function centralizes the logic so it's written and fixed in exactly one place and applied consistently.

What is the airport security analogy illustrating about how middleware functions relate to each other and the final route handler?

Each checkpoint (middleware) either waves the traveler through to the next one or stops them right there if something's wrong, and the gate (route handler) is only reached after passing every checkpoint in sequence — illustrating that middleware forms an ordered chain where each link can pass control on or end things early.

If `app.use(logger)` is registered with no path argument, which requests does it run for?

All of them — app.use() without a path applies to every incoming request that reaches that point in the chain, regardless of which route it ultimately matches.

Can a middleware function modify the `req` object before passing it on? Give an example of why that's useful.

Yes — a middleware can attach new properties to req for later code to use, for example a requireAuth middleware setting req.user after verifying credentials, so the route handler can read req.user without having to re-verify anything itself.

With middleware order `[express.json(), logger, requireAuth, routeHandler]`, why must `express.json()` run before anything that reads `req.body`?

req.body doesn't exist as a parsed object until something has read and parsed the raw incoming request body; express.json() does exactly that, so anything relying on req.body — including requireAuth or the route handler — must run after it in the chain.

What's the difference between middleware meant to run on every request versus middleware scoped to one route or group of routes?

Global middleware (registered with app.use() at the top level) applies to all matching requests regardless of destination, while scoped middleware is passed as an argument to specific route definitions (or app.use() on a specific path/router) and only runs for requests headed there.

Why choose to write logic as middleware rather than calling a regular helper function at the top of each route handler?

Middleware is registered once and automatically applies to every route it's attached to, so adding a new route doesn't require remembering to call the helper again — calling a helper manually only works if every developer remembers to add that call to every relevant handler.

What does it mean for middleware to "short-circuit" the request? Give an example.

It means the middleware ends the request itself instead of passing it further down the chain — for example, requireAuth sending a 401 response and not calling next() when authentication fails, so the route handler (and any later middleware) never runs at all.

Is the route handler itself technically also a kind of middleware in Express?

Functionally, yes — a route handler has the same (req, res, next) shape and sits at the end of the same chain, the main difference being that it's usually the last function called and typically ends the chain by sending a response instead of calling next().

If two middleware functions are both registered with `app.use()` and the first calls `next()`, does the second get its own fresh chance to act, or does it just see the first one's finished response?

It gets its own fresh chance to act on the same req and res objects — calling next() doesn't finalize anything, it simply hands control forward, so the second middleware can still inspect or modify the request and response before anything is sent.

Why write a validation check like "reject requests missing a required header" as middleware instead of repeating it at the start of every relevant route handler?

As middleware, the check is written once and can be attached to however many routes need it (or globally), and adding a new route that needs the same rule requires only attaching the existing middleware rather than rewriting the check.

What real problem would occur if `requireAuth` accidentally called `next()` even when authentication failed?

The chain would continue to the route handler despite the missing or invalid credentials, effectively bypassing the authentication check entirely and exposing protected routes to unauthenticated requests.

Does calling `next()` inside a middleware function execute the next function immediately and synchronously, and why does this matter?

Calling next() is just an ordinary function call, so the next middleware runs immediately and synchronously from that point — unless that next function itself performs asynchronous work (like a database call), in which case its remaining code after that async operation runs later, once that work completes; understanding this matters for reasoning correctly about execution order in a chain that mixes sync and async middleware.

How would you write a middleware function that only logs requests to paths starting with `/api`, while still being registered globally with `app.use()`?

Inside the middleware, check req.path.startsWith("/api") (or similar) before logging, and call next() unconditionally regardless of the result — this lets one globally-registered function selectively act on a subset of requests instead of relying on Express to filter by path.

Why is `app.use(express.json())` still considered middleware even though it doesn't reject or check anything?

Middleware doesn't have to gate or validate — its defining trait is sitting in the request chain and calling next() to pass control on; express.json() does useful transformation work (parsing the raw body into req.body) rather than a check, but it fits the same shape and role as any other middleware function.