Middleware Order

A Recipe That Works in Production

Middleware Order

The order of middleware decides everything. Here's a sequence that keeps an Express app healthy.

3 min read Level 2/5 #express#middleware#order
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 routesreq.body is always undefined
  • Error handler before routes — never fires
  • trust proxy missing — 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 →