Middleware: the chain every request walks
What middleware actually is in Express, how the (req, res, next) signature works, and the exact bug you get when a middleware forgets to call next.

Every Express route you've seen so far had a second function slipped in before the real handler. app.get("/links", requireAuth, (req, res) => {...}). That's middleware, and it's the mechanism nearly everything in Express is built from, including the framework's own express.json().
The signature: (req, res, next)
A middleware function takes three arguments, not two:
function requireAuth(req: Request, res: Response, next: NextFunction) {
const header = req.get("authorization") ?? "";
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
const userId = token ? userIdForToken(db, token) : null;
if (userId === null) {
res.status(401).json({ error: "Unauthorized" });
return;
}
req.userId = userId;
next();
}That's requireAuth, quoted directly from reference/linkstash/src/app.ts. It reads the Authorization header, strips the Bearer prefix if present, and looks up which user (if any) that token belongs to. Two branches, two very different outcomes:
- No valid token: it calls
res.status(401).json(...)and returns, without callingnext(). The request stops here. Nothing after this middleware ever runs. - Valid token: it stashes
userIdon the request object for later handlers to read, then callsnext(), handing control to whatever comes after it in the chain.
That's the entire contract. A middleware either responds and stops the chain, or calls next() and lets it continue. There's no third option, and forgetting which one you did is the single most common middleware bug, so we'll look at it directly in a minute.
A request walking the chain
app.use(express.json()) runs on every request. requireAuth only runs on routes that list it. Picture a request hitting GET /links, with a valid token:
A valid request: every middleware calls next(), the handler runs and sends the response.
Request arrives
express.json() parses the body (there isn't one on a GET, but it runs regardless) and calls next(). requireAuth finds a valid token, sets req.userId, calls next(). Only then does the actual route handler run, and it's the handler, not any middleware before it, that sends the response.
Order matters here in a way that trips people up. app.use(express.json()) has to run before any route that reads req.body. requireAuth has to run before any route that trusts req.userId. Express runs middleware and routes in the exact order you register them, top to bottom, so registering requireAuth after the routes it's meant to protect would silently do nothing.
The bug: forgetting to call next()
Now the same chain, but imagine requireAuth had a bug: it checks the token, logs something, and just... never calls next() and never sends a response either.
requireAuth never calls next() and never responds. The request hangs. Forever.
Request arrives
Nothing crashes. No error appears anywhere. The client's request just sits open until it times out on its own, because Express is waiting for someone, anyone, to either call next() or send a response, and nobody did. This is quietly one of the worse bugs to debug in an Express app precisely because there's no stack trace to Google. The symptom is "this one endpoint just hangs," and the cause is a missing function call three files away.
The rule that prevents this bug
Every code path in a middleware function must end in exactly one of: next(), next(err), or a response method (res.send, res.json, res.end). Check every if/else branch. The real requireAuth above satisfies this: its one branch responds and returns, its other branch sets state and calls next(). There's no third path where neither happens.
Middleware is just functions, stacked
There's nothing special about express.json() or requireAuth as concepts, they're ordinary functions matching one signature, registered in order. That's why you can write your own just as easily. A logging middleware, a rate limiter, a request-id tagger, they all look like this:
function logRequests(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.path}`);
next();
}
app.use(logRequests);app.use with no path applies to every request. app.get(path, middleware, handler) scopes it to one route. Both register the same kind of function, just at different points in the chain.
Express itself only ships a handful of built-ins (express.json(), express.static(), a couple of others), and that's intentional. Most middleware you'll use day to day comes from separate packages: cors for cross-origin headers, helmet for security headers, morgan for request logging. Linkstash keeps its own middleware minimal on purpose, express.json() and requireAuth cover everything the API needs, and that's a reasonable default until you have a specific reason to add more. Every extra middleware in the chain is one more thing running on every request, and one more place a "forgot to call next()" bug can hide.
Order is not decoration
It's worth stating plainly, because it's easy to skim past: app.use(express.json()) before app.use(logRequests) before the routes is not a style choice, it's the actual execution order. Move requireAuth after a route that uses req.userId and that route breaks, silently, with req.userId reading as undefined instead of throwing anything you'd notice in a quick test. When a route behaves like the auth check never ran, the first thing to check is whether it actually runs before that route in the file, not just somewhere in it.
Quick check
A middleware checks something and, on failure, does nothing (no next(), no response). What happens to the request?
You've now seen middleware succeed and middleware hang. There's a third outcome, a middleware or handler that throws, and it gets its own lesson soon because Express 5 changes how it behaves. First, next up: designing a REST API that ages well, which is where the status codes these chains produce (401, 404, and friends) get their reasoning.

Written by
Rhythm Bhiwani
Engineer and relentless builder, happiest reverse-engineering hard problems until they click.
Enjoyed this?
Tap the heart to leave some love.
Be the first to react
Comments
Join the conversation.
Loading comments…


