Your Express app has 40 middleware functions. Some run on every route. Some run on three. Nobody knows what order they execute. Time to fix that.
The Middleware Pipeline
Middleware executes in the order you register it. Think of it as a pipeline: request enters from the left, passes through each function, and the response flows back.
Request -> Logger -> Auth -> Validate -> Handler -> Error Handler -> Response
Request Timing Middleware
function requestTimer(): RequestHandler {
return (req, res, next) => {
const start = process.hrtime.bigint();
res.on("finish", () => {
const ms = Number(process.hrtime.bigint() - start) / 1e6;
logger.info({ method: req.method, path: req.path, status: res.statusCode, ms });
});
next();
};
}
Auth Middleware with JWT
function authenticate(): RequestHandler {
return (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (\!token) return res.status(401).json({ error: "Missing token" });
try { req.user = jwt.verify(token, process.env.JWT_SECRET\!); next(); }
catch { res.status(401).json({ error: "Invalid token" }); }
};
}
Error Handling Middleware
Express error handlers have 4 parameters. They MUST be registered last:
class AppError extends Error {
constructor(public statusCode: number, message: string) { super(message); }
}
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
const status = err instanceof AppError ? err.statusCode : 500;
logger.error({ err, method: req.method, path: req.path });
res.status(status).json({
error: status === 500 ? "Internal server error" : err.message,
});
}
Composing Middleware
Group related middleware into route-specific stacks:
const publicRoutes = [requestTimer(), cors()];
const authRoutes = [...publicRoutes, authenticate(), rateLimiter()];
const adminRoutes = [...authRoutes, requireRole("admin")];
app.get("/api/public", ...publicRoutes, publicHandler);
app.post("/api/orders", ...authRoutes, createOrder);
app.delete("/api/users/:id", ...adminRoutes, deleteUser);
Common Mistakes
- Forgetting next(): Request hangs forever with no response
- Error handler not last: Express only calls the 4-parameter handler if it is registered after all routes
- Not catching async errors: Express 4 does not catch rejected promises. Wrap async handlers or use express-async-errors.
- Leaking internal errors to clients: Never send stack traces or DB errors in production responses
Part of my Production Backend Patterns series. Follow for more practical backend engineering.
If this was useful, consider:
- Sponsoring on GitHub to support more open-source tools
- Buying me a coffee on Ko-fi
Top comments (0)