Global vs. Route-Specific Middleware in Express: A Decision Guide
Deciding between global app.use() middleware and route-specific middleware in Express comes down to scope, cost, and failure modes. This guide compares both and shows a validated implementation.
26 Mar 2026, 00:47 UTC

The decision you're actually making
Every Express middleware function you write has to be registered somewhere, and that choice — app.use() at the top of the file versus an argument on a specific route — determines which requests pay its cost and which requests it can break. Get it wrong in one direction and every endpoint carries latency it doesn't need; get it wrong in the other and an authorization check silently doesn't run on the route that needed it.
The useful rule of thumb: register middleware globally only when every request legitimately needs it. Everything else belongs on the routes that use it.
Comparing the two options
| Criteria | Global (app.use()) | Route-specific |
|---|---|---|
| Scope | Every request reaching the app (after registration point) | Only the routes where it's listed |
| Best for | Logging, body parsing, CORS, request IDs | Authorization, input validation, per-resource guards |
| Cost profile | Paid on all endpoints, including health checks | Paid only where needed |
| Main risk | Heavy work inflates latency app-wide; registration after routes means it never runs for them | Forgetting to add it to a new route that needs it |
| Failure mode | Slow everything, or silently skipped middleware | Unprotected endpoint |
Why execution order matters more than most bugs
Express runs middleware strictly in registration order. A global middleware registered after your routes will never execute for those routes — the route handler already ended the response. This produces a confusing symptom: the middleware exists, no error is thrown, and it simply never fires.
Equally important: every middleware must either call next() or send a response. A middleware that does neither leaves the request hanging until the client times out. This is the most common cause of "my endpoint randomly hangs" reports.
Concrete implementation
The following shows the recommended split: cross-cutting concerns global, protection per-route. Run it with Node 18+ and Express 4 (npm install express), from a file like server.js:
const express = require('express');
const app = express();
// Global: every request needs parsing and logging.
app.use(express.json());
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next(); // required — omitting this hangs every request
});
// Route-specific: only the admin area needs this check.
function requireAdmin(req, res, next) {
if (req.headers['x-admin-token'] !== process.env.ADMIN_TOKEN) {
return res.status(403).json({ error: 'forbidden' });
}
next();
}
app.get('/health', (req, res) => res.json({ ok: true }));
app.get('/admin/users', requireAdmin, (req, res) => {
res.json({ users: [] });
});
app.listen(3000);Note that requireAdmin appears only on the route that needs it. The /health endpoint — which a load balancer may hit every few seconds — never pays for the token check, and the admin check can't accidentally be skipped by a global registration ordering mistake.
Validating the behavior
With the server running, verify from another terminal (no elevated permissions needed):
curl -i http://localhost:3000/health
curl -i http://localhost:3000/admin/users
curl -i -H "x-admin-token: $ADMIN_TOKEN" http://localhost:3000/admin/usersExpected results: the first returns 200, the second returns 403, the third returns 200 with the JSON body. The server log should show all three requests, confirming the global logger ran in every case. To prove the ordering hazard yourself, move the logger's app.use() below the route definitions and re-run — the log lines stop appearing.
Limitations and edge cases
Two caveats worth knowing. First, app.use('/admin', requireAdmin) is a middle path — path-scoped global middleware — useful when a whole subtree shares a guard, but it applies to all methods under that path, which may be broader than you intend. Second, routers (express.Router()) have their own middleware stacks; middleware on a router runs only for routes mounted through that router, which is the cleanest way to scale this pattern in larger apps. Whichever scope you choose, the verification above — one request that should pass the middleware and one that shouldn't — is the fastest way to confirm the wiring is what you think it is.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.