Middleware
The secret that makes Express click: there is no separate "middleware feature" bolted on the side. Middleware is Express. Routing is middleware. Body parsing is middleware. Auth, logging, error handling - all the same shape. Once you see it, the framework stops being a pile of methods to memorize and becomes one idea you understand all the way down.
The mental model: a chain of (req, res, next) functions
📝 Picture a request walking through a hallway of doors. Each door is a function with three things:
req (what came in), res (what you'll send back), and next (a doorknob that opens the next door).
At each door, the function does one of three things:
- Read or modify
req/res, then callnext()to pass the request along. - End the response itself (
res.send(...),res.json(...)) - the request stops here. - Call
next()with nothing changed, just to hand off.
A middleware function looks exactly like this:
What just happened: three parameters, and a decision about whether to call next() - the shape of
every middleware in Express. A route handler like app.get('/tasks', (req, res) => ...) is the same
thing; it just happens to be the last door, so it sends a response instead of calling next().
Routes are middleware bound to a method and a path.
💡 If you read Build a Server with node:http, you already met
(req, res) and "call the next function to keep going." Express didn't invent this - it formalized it.
Writing your first middleware: a request logger
The classic first middleware logs every request: method, URL, status, and duration.
const = ;
const = ;
; // runs for EVERY request
;
;
What just happened: app.use(logger) registers logger to run at the start of every request. We
record start, then attach a one-time listener to res's 'finish' event - fired when the response
is fully sent - so we can log the real status code and duration. logger calls next() immediately;
it doesn't wait for finish. Its job in the chain is to step aside and let the request continue. Hit
GET /tasks and you'll see GET /tasks 200 2ms.
Three places you can register middleware
; // GLOBAL: every request
; // PATH-SCOPED: only paths under /admin
; // PER-ROUTE: only this route, in this order
What just happened: same mechanism, three reaches. app.use(fn) runs on everything. app.use('/admin', fn)
runs only when the path starts with /admin. Listing functions inside a route runs them left to right
for that one route. The middleware doesn't change - only where you mount it does.
⚠️ Order matters (this is the part that bites everyone)
Middleware runs in the order you register it, top to bottom. Getting this wrong is the most common Express bug there is.
Trap 1: the parser must come before the routes that need it. express.json() reads the request
body into req.body. Register your routes before it, and req.body is undefined there.
// ❌ WRONG - route runs before the body is parsed
;
;
// ✅ RIGHT - parse first, then route
;
;
What just happened: in the wrong version, the handler runs before express.json() has parsed
anything, so req.body is undefined. In the right version, parsing happens earlier in the chain, so
by the time your handler runs, req.body is already populated. Order on the page = order at runtime.
Trap 2: forgetting next() hangs the request. A middleware that neither responds nor calls
next() is a closed door with no knob - the request stands there forever, until the client times out.
// ❌ This middleware silently hangs every request
;
What just happened: this is the number-one beginner bug. The function logs and returns, but never
calls next() or sends a response, so Express has no way to know it's done. Every middleware must do
exactly one of two things: respond, or call next(). If a request hangs for no reason, check
for a missing next() first.
Built-in and third-party middleware
You rarely write parsers and security headers yourself - you reach for middleware that already exists.
Built into Express:
express.json()- parses JSON request bodies intoreq.body.express.urlencoded({ extended: true })- parses HTML form submissions intoreq.body.express.static('public')- serves files from a folder (images, CSS, the front end).
Popular third-party packages (install with npm, then app.use(...) them):
cors- adds the headers browsers need to allow cross-origin requests.morgan- a polished request logger (like ours, but configurable).helmet- sets a bundle of security-related HTTP headers.
const = ;
const = ;
; // security headers on every response
; // allow cross-origin requests
; // parse JSON bodies
; // log requests
What just happened: each line plugs a ready-made middleware into the chain, listed near the top so
they run early - security headers and CORS should apply to every response, and express.json() must
come before any route reading req.body (Trap 1). Helmet, cors, json, a logger: the boring, sensible
opening for most real Express apps.
Passing data down the chain with req
Middleware can attach things to req, and every later function in the chain can read them. The
textbook case is authentication: one middleware checks who's calling and stashes the user on req.
;
;
;
What just happened: no header means an unauthenticated request, so requireAuth responds 401 and
stops - note there's no next() after it. A header sets req.user and calls next(), so by the
time /tasks runs, req.user is populated and the route trusts that auth already happened upstream.
Two halves of the pattern: reject and stop, or enrich req and continue. The return matters
- without it, the code would respond and fall through to
next(), trying to send two responses.
Recap
- Middleware is a
(req, res, next)function in a chain. It can read/modifyreq/res, end the response, or callnext(). Routes are middleware bound to a method and path. - Register at three scopes: global (
app.use(fn)), path-scoped (app.use('/admin', fn)), or per-route (app.get(path, mw, handler)). - Order matters. Middleware runs top to bottom.
express.json()must come before any route that readsreq.body. - A middleware that neither responds nor calls
next()hangs the request - the #1 beginner bug. - Use built-ins (
express.json,express.urlencoded,express.static) and third-party packages (cors,morgan,helmet) instead of writing your own. - Share work down the chain by attaching to
req(e.g. an auth middleware setsreq.user); reject withres.status(401)and don't callnext().
Quick check
[
{
"q": "What are the three parameters of a standard Express middleware function?",
"choices": ["req, res, done", "request, response, callback", "req, res, next", "app, req, res"],
"answer": 2,
"explain": "Standard middleware is (req, res, next): read/modify req and res, then call next() to pass control along."
},
{
"q": "You register your POST route before express.json(). What happens to req.body in that route?",
"choices": ["It contains the parsed JSON", "It is undefined because nothing parsed the body yet", "It throws an error", "Express auto-reorders the middleware for you"],
"answer": 1,
"explain": "Middleware runs in registration order. If express.json() is registered after the route, the route runs first and req.body is undefined."
},
{
"q": "A middleware logs a message but never calls next() and never sends a response. What is the result?",
"choices": ["The next middleware runs anyway", "Express sends an automatic 404", "The request hangs until it times out", "The response is sent with status 200"],
"answer": 2,
"explain": "Every middleware must either respond or call next(). Doing neither leaves the request with nowhere to go, so it hangs - the most common beginner bug."
}
]
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. What are the three parameters of a standard Express middleware function?
2. You register your POST route before express.json(). What happens to req.body in that route?
3. A middleware logs a message but never calls next() and never sends a response. What is the result?