Middleware is code that sits between the raw incoming request and the final route handler. It is the most reusable structure in web frameworks — auth checks, request logging, rate limiting, CORS headers, and body parsing all live there. Once you understand the middleware pattern, you understand how Express, FastAPI, ASP.NET Core, Rails, and modern edge runtimes are all built on the same conceptual foundation.
What changed in 2026
- Edge middleware is now first-class. Vercel Edge Middleware, Cloudflare Workers, and Next.js middleware run at the CDN layer — before your origin receives the request. Auth and A/B decisions happen globally in < 10 ms.
- AI pipeline middleware emerged. LLM serving stacks (LiteLLM, Portkey, OpenRouter) expose middleware-style hooks for guardrails, prompt caching, cost tracking, and PII redaction.
- Hono.js middleware ecosystem grew rapidly as the lightweight framework became standard for Cloudflare Workers and Bun. Its middleware API is 95% compatible with Express.
- FastAPI's dependency injection system (which is middleware by another name) became a canonical pattern for Python async middleware after Python 3.12 performance improvements.
The middleware pattern
A middleware function receives the request, performs some action, and either terminates the request (returns a response) or passes control to the next middleware:
// Express-style middleware signature
function myMiddleware(req, res, next) {
// Before handler
console.log(`${req.method} ${req.path}`);
next(); // Pass to next middleware or route handler
// After handler (in frameworks that support two-phase)
}
The key concept: each middleware decides whether to call next(). If it does not, the chain stops. If it does, control passes down the chain and (in Express-style) resumes here after the downstream middleware completes.
Middleware in Express / Hono
import { Hono } from "hono";
import { logger } from "hono/logger";
import { cors } from "hono/cors";
import { jwt } from "hono/jwt";
const app = new Hono();
// Runs for every request
app.use("*", logger());
app.use("*", cors({ origin: "https://app.example.com" }));
// Runs only for /api/* routes
app.use("/api/*", jwt({ secret: process.env.JWT_SECRET }));
// Route handler — only reached if auth middleware calls next()
app.get("/api/users", async (c) => {
const payload = c.get("jwtPayload");
return c.json({ userId: payload.sub });
});
Middleware registered with app.use("*") runs for every request in registration order. Route-specific middleware runs only for matching paths.
Middleware in Python / FastAPI
FastAPI uses Starlette's ASGI middleware stack:
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
import time
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def add_timing(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request) # call downstream
duration = time.perf_counter() - start
response.headers["X-Response-Time"] = f"{duration:.4f}s"
return response
FastAPI's Depends() system functions as middleware for dependency injection — auth, database sessions, and feature flags are common use cases.
Edge middleware (Vercel / Next.js)
// middleware.ts (Next.js App Router — runs at the CDN edge)
import { NextRequest, NextResponse } from "next/server";
import { verifyJWT } from "./lib/auth";
export async function middleware(req: NextRequest) {
const token = req.cookies.get("session")?.value;
if (!token || !(await verifyJWT(token))) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/api/protected/:path*"],
};
This executes at Vercel's edge nodes, globally distributed — unauthenticated users are redirected before the request ever reaches your origin server.
Middleware execution order
| Layer |
Runs when |
| Global middleware |
Every request |
| Path-scoped middleware |
Requests matching path pattern |
| Route handler |
After all middleware calls next() |
| Error handler middleware |
When any middleware/handler throws |
| Response middleware (two-phase) |
After route handler returns |
Order of registration equals order of execution. Error-handling middleware goes last.
app.use(logger); // 1st
app.use(cors); // 2nd
app.use("/api", authJwt); // 3rd (only for /api)
app.get("/api/data", handler); // 4th — only if authJwt calls next()
app.use(errorHandler); // LAST — catches errors from any above
Common patterns
Request ID tracing:
app.use((req, res, next) => {
req.id = req.headers["x-request-id"] || crypto.randomUUID();
res.setHeader("X-Request-Id", req.id);
next();
});
Rate limiting:
import rateLimit from "express-rate-limit";
const limiter = rateLimit({ windowMs: 60_000, max: 100 });
app.use("/api/", limiter);
Response caching header injection:
app.use("/static/*", (c, next) => {
c.header("Cache-Control", "public, max-age=86400");
return next();
});
How to design middleware
- One concern per middleware — do not combine auth + logging + compression in one function.
- Always handle errors — if a middleware throws without calling next(error), the request hangs. Wrap async middleware in try/catch or use a wrapper helper.
- Be mindful of execution order — logging should come before auth so you capture failed auth attempts; compression should come after response body is set.
- Scope tightly — apply middleware to the narrowest path prefix that needs it.
- Test middleware in isolation — pass a mock
req/res/next and assert on side effects.
Common mistakes
Forgetting to call next(). If middleware does not call next() and does not send a response, the request hangs forever. Timeouts are the only escape.
Mutating req with untyped properties. In TypeScript, extend the Request type or use a typed context object (c.set('user', payload) in Hono) to avoid type errors downstream.
Putting async code in Express middleware without error propagation. Express 4 does not catch async errors automatically. Use express-async-errors or wrap handlers: app.use((req, res, next) => handler(req, res).catch(next)). Express 5 (stable in 2025) handles this natively.
Applying expensive middleware globally. Rate limiting, JWT verification, and database lookups applied to /healthz and static asset paths waste resources. Scope them to /api/*.
Relying on middleware order without testing it. When middleware is added in multiple files or plugins, the execution order can surprise you. Add an integration test that verifies the chain.
What to skip
- Business logic in middleware — middleware is for cross-cutting concerns. Anything domain-specific belongs in the route handler or service layer.
- Deeply nested middleware chains that are hard to trace — flatten them into a clear list of
app.use() calls.
- Rolling your own auth middleware from scratch without a tested library — auth is subtle; use Passport.js, jose, or your framework's recommended option.
FAQ
Is middleware the same as a plugin?
Similar but not identical. A plugin typically registers middleware, routes, and decorators together. Middleware is the specific request/response interception pattern.
Can middleware run after the response is sent?
In Node.js and most frameworks, you cannot modify the response after headers are sent. Middleware that logs response time must capture the timing before res.end() fires, typically by wrapping the response object.
What is ASGI middleware in Python?
ASGI (Asynchronous Server Gateway Interface) is the async equivalent of WSGI. ASGI middleware wraps the inner app, intercepts scope/receive/send calls, and is the foundation of Starlette and FastAPI middleware.
How does middleware differ from an interceptor in NestJS or Spring?
Conceptually the same pattern — NestJS interceptors add before/after semantics around a RxJS observable; Spring HandlerInterceptors have preHandle/postHandle/afterCompletion. The terminology differs but the pipeline model is identical.
Where to go next