Middleware is Express's core idea: logging, parsing, auth, CORS and error handling are all middleware.
A middleware is a function (req, res, next) that runs during a request. It can do any of three things:
- Respond and end the cycle (
res.status(401).json(...)). - Modify
req/resand callnext()to pass control to the next middleware or route. - Fail by calling
next(err)(or, in Express 5, throwing or rejecting), which skips straight to error-handling middleware.
If it does none of these, the request hangs.
app.use(fn) runs fn for every request; app.use("/admin", fn) only for paths starting with /admin (a prefix match: /admin, /admin/users, not /administrator). Inside, req.url is relative to the mount path while req.originalUrl keeps the full URL. Route handlers are middleware too — app.get(path, a, b, c) is a chain of three.
Order is everything. Express runs matching middleware and routes in exactly the order you registered them. Typical order:
- Request-wide setup: request ID, logging, security headers, CORS, body parsers.
- Authentication (populating
req.user). - Routes and routers.
- A 404 handler for anything unmatched.
- Error handlers (four arguments) last.
A logger registered after your routes never sees requests that routes answered; a body parser registered after a route leaves req.body undefined in that route.
To run code after the response is sent — timing, metrics, logs — don't wait in the middleware; listen for res.on("finish"). Because async middleware is supported in Express 5, you can await inside and simply call next() when done; a rejection is forwarded as an error.
Use res.locals to pass per-request data to later middleware and templates without polluting req.
Example
import express from "express";
const app = express();
// 1. Timing: register first so it sees every request
app.use((req, res, next) => {
const start = performance.now();
res.on("finish", () => {
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${(performance.now() - start).toFixed(1)}ms`);
});
next();
});
app.use(express.json());
// 2. Path-scoped async middleware
app.use("/admin", async (req, res, next) => {
const user = await lookupUser(req.get("authorization")); // a rejection -> error handler
if (!user?.isAdmin) return res.status(403).json({ error: "admins only" });
res.locals.user = user;
next();
});
app.get("/admin/stats", (req, res) => res.json({ viewer: res.locals.user.name }));
app.get("/", (req, res) => res.send("home"));
async function lookupUser(header) { return header === "Bearer admin" ? { name: "ada", isAdmin: true } : null; }
app.listen(3000);Remember
- Middleware either responds, calls
next(), or passes an error app.use(path, fn)is a prefix match; registration order is execution order- Use
res.on('finish')for after-response work andres.localsfor per-request data
Watch out for
- Registering body parsers or auth after the routes that need them.
- Forgetting to call
next()in a non-responding middleware, hanging the request.