A Recipe That Works in Production
Middleware Order
The order of middleware decides everything. Here's a sequence that keeps an Express app healthy.
What you'll learn
- Order middleware sensibly
- Recognize ordering bugs
- Adapt the recipe to your app
A robust order for middleware in a typical Express app:
const app = express();
// 1. trust the proxy (when you're behind nginx / cloudflare)
app.set("trust proxy", 1);
// 2. observability
app.use(requestId); // assign req.id
app.use(pinoHttp({ logger })); // structured logging
// 3. security
app.use(helmet());
app.use(cors(corsOptions));
// 4. parse early
app.use(cookieParser(secret));
app.use(express.json({ limit: "100kb" }));
app.use(express.urlencoded({ extended: true }));
// 5. throttle
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
// 6. responses (compress AFTER routes have set Content-Type — see note)
app.use(compression());
// 7. auth (cookie-based or token-based)
app.use(loadCurrentUser); // attaches req.user if logged in
// 8. routes
app.use("/api", apiRouter);
// 9. 404
app.use((req, res) => res.status(404).json({ error: "not found" }));
// 10. errors — must be last
app.use(errorHandler); Notes
trust proxy
If a reverse proxy fronts Node (Nginx, Cloudflare, Heroku), set
trust proxy so Express uses X-Forwarded-For for req.ip and
X-Forwarded-Proto for req.protocol. Crucial for rate-limit
keys and Secure cookies.
Logging Goes Early
You want every request logged — including ones that fail. Mount logging before security middleware so even rejected requests appear.
compression Position
Mount compression early so it sees all responses. (Some guides
mount it after routes; either works because Express collects all
middleware into a chain regardless.)
Auth After Parsing
Auth often reads cookies or bodies. Mount it after cookie-parser
and express.json.
Error Handler LAST
Always last. Anything after won’t fire on error.
Antipatterns
- Logger after routes — successful requests log; failures don’t
- Body parser after routes —
req.bodyis alwaysundefined - Error handler before routes — never fires
trust proxymissing — every rate-limit key is your proxy’s IP, effectively no rate-limiting
Customize Freely
This is a recipe, not a law. Adapt to your app — add session middleware, swap morgan for pino, drop compression if a CDN does it.
The principle: middleware runs in order, and order encodes intent.
Conditional Middleware →