Middleware Pattern
Build request pipelines from small, composable functions that each handle one concern before passing control to the next handler.
Recipe
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.
Working Example
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:
- Middleware is a function with signature
(req, res, next) - Call
next()to continue; omit it to end the response - Order matters: parsers before routes, error handlers last
- Route-level middleware (
requireAuth) scopes auth to specific paths
Deep Dive
How It Works
- Each middleware can mutate
req/res, end the response, or callnext() - The chain is a linked invocation:
m1 -> m2 -> m3 -> route handler - Error middleware has 4 parameters
(err, req, res, next)and only runs whennext(err)is called - Fastify uses hooks (
onRequest,preHandler) instead of a single chain; NestJS uses guards, interceptors, and pipes
Middleware Categories
| 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 |
TypeScript Notes
// 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();
}Gotchas
- Calling
next()afterres.send()- sends double responses and crashes withERR_HTTP_HEADERS_SENT. Fix: return immediately after ending the response. - Forgotten
next()- request hangs until timeout. Fix: every code path must callnext()or end the response. - Error middleware registered before routes - never runs. Fix: register 4-arg handlers after all routes.
- Async middleware without error forwarding - rejected promises are lost in older patterns. Fix: Express 5 forwards async errors; otherwise use
try/catchandnext(err). - Global auth on static files - blocks
favicon.icoand health checks. Fix: mount auth on specific routers, notapp.use(auth)globally. - Order of body parsers -
express.json()after routes means POST bodies are never parsed. Fix: parsers first. See Middleware Ordering.
Alternatives
| 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 |
FAQs
What is the difference between app.use(fn) and app.get(path, fn)?
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).
How many middleware functions is too many?
No hard limit, but if the chain exceeds 10 global middlewares, audit for redundancy. Combine logging + request ID into one middleware.
Can middleware be async?
Yes. Express 5 automatically catches async errors. In raw Node or Express 4, wrap with a helper that calls next(err) on rejection.
How does this relate to Fastify plugins?
Fastify plugins encapsulate routes + hooks + decorators in a scope. Middleware is flat; plugins prevent decorator leakage between feature modules.
Should I use middleware for business logic?
No. Middleware handles cross-cutting infrastructure. Business rules belong in service functions called by thin route handlers.
How do I test middleware in isolation?
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.
What about Koa's ctx pattern?
Koa uses async middleware with ctx instead of (req, res, next). See Koa & Polka.
Does middleware run on 404 requests?
Only if registered before the 404 handler. Add a catch-all route or error middleware at the end of the chain.
Related
- Middleware Ordering - correct registration sequence
- Error Handling in Express - error middleware patterns
- Security Middleware - helmet, cors, rate limits
- Guards, Interceptors & Pipes - NestJS equivalent
- HTTP Fundamentals Best Practices - section checklist
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.