Middleware Pattern
Build request pipelines from small, composable functions that each handle one concern before passing control to the next handler.
Search across all documentation pages
Build request pipelines from small, composable functions that each handle one concern before passing control to the next handler.
Quick-reference recipe card - copy-paste ready.
import express, { type Request, type Response, type NextFunction } from "express";
const app = express();
// Middleware runs in registration order
app.use((req, _res, next) => {
req.headers["x-request-start"] = String(Date.now());
next();
});
app.get("/users", (req, res) => {
res.json({ startedAt: req.headers["x-request-start"] });
});
// Four-argument error middleware (Express 5)
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
res.status(500).json({ error: err.message });
});When to reach for this: Any HTTP app that needs cross-cutting concerns (logging, auth, parsing, CORS) applied consistently across routes.
A minimal middleware runner without Express, then the same chain in Express 5.
import express from "express";
// --- Plain Node middleware types ---
type Req = { headers: Record<string, string | undefined>; body?: unknown };
type Res = { statusCode: number; body?: unknown; status(n: number): Res; json(d: unknown): void };
type Next = (err?: Error) => void;
type Middleware = (req: Req, res: Res, next: Next) => void;
function runMiddleware(
middlewares: Middleware[],
req: Req,
res: Res,
finalHandler: () => void
) {
let index = 0;
const next: Next = (err) => {
if (err) throw err;
const fn = middlewares[index++];
if (fn) fn(req, res, next);
else finalHandler();
};
next();
}
// --- Express 5 equivalent ---
const app = express();
app.use(express.json({ limit: "1mb" }));
function requestId(req: express.Request, _res: express.Response, next: express.NextFunction) {
req.headers["x-request-id"] = crypto.randomUUID();
next();
}
function requireAuth(req: express.Request, res: express.Response, next: express.NextFunction) {
const token = req.headers.authorization;
if (!token?.startsWith("Bearer ")) {
res.status(401).json({ error: "Unauthorized" });
return;
}
next();
}
app.use(requestId);
app.get("/public", (_req, res) => res.json({ ok: true }));
app.get("/private", requireAuth, (_req, res) => res.json({ secret: true }));What this demonstrates:
(req, res, next)next() to continue; omit it to end the responserequireAuth) scopes auth to specific pathsreq/res, end the response, or call next()m1 -> m2 -> m3 -> route handler(err, req, res, next) and only runs when next(err) is calledonRequest, preHandler) instead of a single chain; NestJS uses guards, interceptors, and pipes| Category | Examples | Typical position |
|---|---|---|
| Request parsing | express.json(), express.urlencoded() | Early |
| Security | helmet, cors, rate limiter | After parsers |
| Context | request ID, logging, timing | Before routes |
| Auth | JWT verify, API key check | Per-route or router group |
| Business | Route handlers | Middle |
| Error handling | 4-arg error middleware | Last |
// Extend Express Request for typed middleware context
declare global {
namespace Express {
interface Request {
userId?: string;
}
}
}
function attachUser(req: express.Request, _res: express.Response, next: express.NextFunction) {
req.userId = "user-42";
next();
}next() after res.send() - sends double responses and crashes with ERR_HTTP_HEADERS_SENT. Fix: return immediately after ending the response.next() - request hangs until timeout. Fix: every code path must call next() or end the response.try/catch and next(err).favicon.ico and health checks. Fix: mount auth on specific routers, not app.use(auth) globally.express.json() after routes means POST bodies are never parsed. Fix: parsers first. See Middleware Ordering.| Alternative | Use When | Don't Use When |
|---|---|---|
| Fastify hooks | Need lifecycle stages (preValidation, preSerialization) | Team only knows Express |
| NestJS guards/pipes | Decorator-driven cross-cutting with DI | Simple CRUD API |
| Inline handler logic | Single-endpoint script | More than 2 shared concerns |
| Hono middleware | Edge + Node with tiny bundle | Heavy Express plugin ecosystem required |
app.use matches all methods and paths (unless a path prefix is given). app.get only matches GET requests to a specific path. Auth middleware usually goes on app.use("/api", auth).
No hard limit, but if the chain exceeds 10 global middlewares, audit for redundancy. Combine logging + request ID into one middleware.
Yes. Express 5 automatically catches async errors. In raw Node or Express 4, wrap with a helper that calls next(err) on rejection.
Fastify plugins encapsulate routes + hooks + decorators in a scope. Middleware is flat; plugins prevent decorator leakage between feature modules.
No. Middleware handles cross-cutting infrastructure. Business rules belong in service functions called by thin route handlers.
Call the function with mock req, res, and a next spy. Assert next was called, or res.status was set. Supertest tests the full chain.
Koa uses async middleware with ctx instead of (req, res, next). See Koa & Polka.
Only if registered before the 404 handler. Add a catch-all route or error middleware at the end of the chain.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 18, 2026