Last 30 Days
No notifications
Middleware functions have access to the request, response, and next function. They execute in order and can modify req/res or end the cycle.
Request β MW 1 β MW 2 β MW 3 β Route Handler β Response
β β β
βββnext()βββnext()β| Type | Example | Purpose |
| Built-in | express.json() | Parse JSON bodies |
| Third-party | cors(), morgan() | CORS, logging |
| Custom | authMiddleware | Authentication |
| Error | (err, req, res, next) | Error handling |
| Route-level | router.use() | Scoped to router |
// Regular middleware: 3 params
function myMiddleware(req, res, next) {
// Do something
next(); // Pass to next
}// Error middleware: 4 params (must have all 4!)
function errorHandler(err, req, res, next) {
res.status(500).json({ error: err.message });
}
A middleware is a function with the signature (req, res, next) => { β¦ } that runs *between* the request arriving and the route handler responding. Each one can:
req (e.g. parse the body, attach req.user).res (e.g. add headers).res.json(β¦)) and stop the chain.next().next(err).function logger(req, res, next) {
console.log(${req.method} ${req.url});
next(); // hand off to the next middleware / route
}app.use(logger);
Every request now logs before doing anything else.
app.use(express.json()); // parse JSON bodies
app.use(express.urlencoded({ extended: true })); // parse form posts
app.use(express.static('public')); // serve files from /public
app.use(cors()); // CORS headers
app.use(helmet()); // security headers
app.use(morgan('dev')); // request logger
app.use(cookieParser()); // parse cookiesMost "how do I do X in Express" questions resolve to "add this middleware".
// 1. Globally β runs for every request
app.use(logger);// 2. Path-scoped β only for URLs under /api/admin
app.use('/api/admin', requireAdmin);
// 3. Per-route β just for this endpoint
app.get('/profile', requireAuth, (req, res) => res.json(req.user));
You can chain as many as you want before the final handler:
app.post('/posts', requireAuth, validatePost, createPost);1. Forgetting next(). The request hangs forever β client times out. Either call next() *or* end the response.
2. Calling next() *and* res.send(β¦). The next middleware tries to send again β Cannot set headers after they are sent.
3. Wrong order. app.use(routes); app.use(express.json()); β routes see req.body === undefined.
4. Error handler with 3 args. Express only treats it as an error handler if it has four parameters (err, req, res, next).
5. Async errors not forwarded. throw inside an async middleware in Express 4 won't reach your error handler unless you try/catch and call next(err).
app.use(helmet()); // 1. security headers first
app.use(cors()); // 2. CORS
app.use(express.json()); // 3. parse body
app.use(morgan('dev')); // 4. log
app.use(rateLimit(...)); // 5. throttle
app.use(authenticate); // 6. set req.user (does NOT block anonymous)
app.use('/api', apiRoutes); // 7. real routes
app.use(notFound); // 8. 404 β only reached if nothing matched
app.use(errorHandler); // 9. errors LASTA middleware can only protect what comes after it.
function maybeAuth(req, res, next) {
if (req.path.startsWith('/public')) return next(); // skip
return requireAuth(req, res, next);
}Or use app.use('/private', requireAuth) so it never runs on the public branch in the first place β cleaner.
A function that returns a middleware lets you parametrise behaviour:
function requireRole(role) {
return (req, res, next) => {
if (req.user?.role !== role) return res.status(403).json({ error: 'forbidden' });
next();
};
}app.delete('/posts/:id', requireAuth, requireRole('admin'), deletePost);
A simple in-memory rate limiter:
function rateLimit(max, windowMs) {
const hits = new Map();
return (req, res, next) => {
const now = Date.now(), key = req.ip;
const list = (hits.get(key) || []).filter(t => t > now - windowMs);
if (list.length >= max) return res.status(429).json({ error: 'slow down' });
list.push(now); hits.set(key, list); next();
};
}
app.use(rateLimit(100, 60_000)); // 100 req/min/IP(For real apps use express-rate-limit β it handles distributed stores, headers, etc.)
// Wrapper that forwards rejections to next()
const asyncH = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);const requireAuth = asyncH(async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'no token' });
req.user = await verifyJwt(token);
next();
});
Express 5 (current stable) catches thrown async errors automatically; in v4 the wrapper above is the standard idiom.
Four parameters tell Express "I'm an error handler":
app.use((err, req, res, next) => {
// your custom AppError class can carry .statusCode and .code
const status = err.statusCode || 500;
res.status(status).json({
error: { code: err.code || 'internal', message: err.message },
...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
});
});Anything that calls next(err) lands here. Always last.
req Is a SuitcaseMiddlewares pile context onto req so later code doesn't need to redo work:
requireAuth β sets req.user
requestId β sets req.id (for log correlation)
locale β sets req.locale
validate(...) β sets req.validatedBy the time the route handler runs, everything it needs is already on req.
A Router is *itself* middleware:
import adminRouter from './routes/admin.js';
app.use('/admin', requireAdmin, adminRouter);You can compose middleware stacks per-feature, mount them, and even nest routers many levels deep.
Middlewares like compression() wrap res.write/res.end. If you also write the response *manually as a stream*, make sure compression sits before the route, and don't pre-set Content-Length when streaming β chunked encoding will sort itself out.
Every middleware costs a few Β΅s and a function call. Tips:
/upload).express-* middlewares β they're battle-tested for hot paths.fs.readFileSync per request.app.use((req, res, next) => {
req.id = crypto.randomUUID();
req.start = process.hrtime.bigint();
res.on('finish', () => {
const ms = Number(process.hrtime.bigint() - req.start) / 1e6;
log.info({ id: req.id, m: req.method, u: req.url, s: res.statusCode, ms });
});
next();
});A single req.id lets you correlate logs across DB calls, downstream APIs, and error traces.
1. Write a logger middleware that prints METHOD URL β status (ms) for every request (use res.on('finish')).
2. Write requireAuth that fakes auth: 401 unless req.headers['x-user'] exists; otherwise sets req.user = { name: req.headers['x-user'] }. Mount it on /api.
3. Build a requireRole('admin') factory and protect a DELETE /api/posts/:id route with it.
4. Add a 4-arg error handler that maps a custom HttpError class with .statusCode to the right HTTP status.