If you understand middleware, you understand Express. Everything in Express — routing, body parsing, auth, error handling — is built on this one idea.
In simple language — middleware is a function that runs between the request coming in and the response going out. It gets the request and response objects, and a next function. It can:
- Run any code (logging, auth checks)
- Modify
reqorres(attachreq.user, set headers) - End the request (send a response itself)
- Call
next()to pass to the next middleware
The whole Express app is a pipeline of these functions executed in order.
The signature
function middleware(req, res, next) {
// ... do something
next(); // pass to the next middleware
}
That’s it. Three arguments. Call next() when done.
A simple example
const express = require('express');
const app = express();
// Middleware 1: log every request
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Middleware 2: attach a timestamp
app.use((req, res, next) => {
req.requestTime = Date.now();
next();
});
// Route handler (also middleware, just the last one)
app.get('/', (req, res) => {
res.json({ at: req.requestTime });
});
app.listen(3000);
When GET / comes in:
- Logger runs, calls
next() - Timestamp middleware runs, calls
next() - Route handler runs, sends response
ORDER MATTERS (this is the #1 gotcha)
Express runs middleware in the order they’re registered. If body parser comes after the route, the route sees req.body === undefined:
// BROKEN
app.post('/users', (req, res) => {
res.json(req.body); // undefined!
});
app.use(express.json()); // too late
// CORRECT
app.use(express.json());
app.post('/users', (req, res) => {
res.json(req.body); // works
});
Rule of thumb — register middleware in this order:
- Logging
- Security (helmet, cors)
- Body parsing (express.json, express.urlencoded)
- Auth
- Routes
- 404 handler
- Error handler (last, always)
Three ways to use middleware
// 1. App-level — runs for every request
app.use(logger);
// 2. Path-specific — only for requests starting with /api
app.use('/api', apiMiddleware);
// 3. Route-specific — only for this one route
app.get('/admin', requireAuth, (req, res) => { ... });
Short-circuiting
A middleware can end the request itself by sending a response and NOT calling next():
const requireAuth = (req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
}
next(); // only continues if authorized
};
The return is critical — without it, both the response AND next() could fire, which causes “Cannot set headers after they are sent.”
Error-handling middleware
A special type — four arguments. Express recognizes it by arity:
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
To send an error into this, call next(err) with an argument:
app.get('/users/:id', async (req, res, next) => {
try {
const user = await db.findUser(req.params.id);
if (!user) return next(new Error('Not found'));
res.json(user);
} catch (err) {
next(err); // jumps to error middleware
}
});
Always register the error handler last, after all routes.
Reusing middleware
Middleware is just a function — we can publish it to npm, import from anywhere, share across projects. That’s why the Express ecosystem is huge: cors, helmet, morgan, compression, cookie-parser, express-rate-limit are all just middlewares.
The mental model
“An Express app is an ordered list of functions that each get a chance to read/modify the request, send a response, or pass control to the next one.”
If you internalize that, the rest of Express falls out naturally — routes are just middlewares matched by path/method, error handlers are middlewares with 4 args, routers are sub-pipelines.