Application-Level Middleware
Application-level middleware is the most common kind you’ll write in Express: functions bound directly to the app instance that run as requests flow through the pipeline. You attach them with app.use() or with a routing method like app.get(), and an optional mount path lets you decide whether a function fires for every request or only for a branch of your URL space. Because Express executes these functions in registration order, understanding where you place app.use() calls is the difference between a predictable pipeline and a confusing one.
Global middleware with app.use(fn)
Calling app.use() with a single function argument and no path registers global middleware. It runs for every incoming request, regardless of method or URL, before Express reaches any matching route handler. This is where cross-cutting concerns belong — request logging, timing, body parsing, and attaching shared values to req.
const express = require("express");
const app = express();
// Global: runs for every request, any method, any path
app.use((req, res, next) => {
req.requestTime = Date.now();
console.log(`${req.method} ${req.originalUrl}`);
next();
});
app.get("/", (req, res) => {
res.send(`Home, handled at ${req.requestTime}`);
});
app.get("/about", (req, res) => {
res.send(`About, handled at ${req.requestTime}`);
});
app.listen(3000, () => console.log("Listening on http://localhost:3000"));
Output:
$ curl http://localhost:3000/about
GET /about
About, handled at 1749859200000
The logger and the timing logic ran for /about even though we never mentioned that path — that’s the defining trait of global middleware. Every request passes through it, so anything you set on req here is available to all downstream handlers.
Tip: Always call
next()(or send a response) inside middleware. If you forget, the request hangs forever because Express never advances to the next function in the stack.
Path-scoped middleware with app.use(‘/path’, fn)
Supply a string as the first argument and the middleware becomes path-scoped: it only runs when the request path starts with that mount path. The match is a prefix match, not an exact one — /users matches /users, /users/42, and /users/42/posts alike. This lets you apply behavior to an entire branch of your API without touching unrelated routes.
// Only runs for requests whose path begins with /admin
app.use("/admin", (req, res, next) => {
if (req.headers["x-admin-token"] !== "secret") {
return res.status(403).json({ error: "Forbidden" });
}
next();
});
app.get("/admin/dashboard", (req, res) => {
res.json({ panel: "metrics" });
});
app.get("/public", (req, res) => {
res.send("Anyone can see this");
});
Output:
$ curl -i http://localhost:3000/admin/dashboard
HTTP/1.1 403 Forbidden
{"error":"Forbidden"}
$ curl http://localhost:3000/public
Anyone can see this
The guard fired for /admin/dashboard but was skipped entirely for /public. One subtlety worth knowing: inside path-scoped middleware, req.url is stripped of the mount path (it becomes /dashboard), while req.originalUrl always preserves the full incoming path (/admin/dashboard).
| Form | Mount path | Runs for | Typical use |
|---|---|---|---|
app.use(fn) | none | every request | logging, parsers, timing |
app.use('/api', fn) | /api | requests under /api | versioned API guards |
app.get('/api', fn) | /api | GET /api only | route handlers |
Method-specific app.use combined with verbs
app.use() is method-agnostic — it matches GET, POST, DELETE, and everything else. When you want middleware that runs for a specific path and a specific HTTP verb, reach for the routing methods (app.get, app.post, etc.). These accept the same middleware functions and can chain several before the final handler, each calling next() to pass control along.
const express = require("express");
const app = express();
app.use(express.json()); // global body parser
function validateBody(req, res, next) {
if (!req.body.email) {
return res.status(400).json({ error: "email is required" });
}
next();
}
// Middleware chained on a single verb + path
app.post("/users", validateBody, async (req, res) => {
const user = { id: 1, email: req.body.email };
res.status(201).json(user);
});
// A GET to /users never triggers validateBody
app.get("/users", (req, res) => {
res.json([{ id: 1, email: "[email protected]" }]);
});
app.listen(3000);
Output:
$ curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" -d '{}'
{"error":"email is required"}
$ curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" -d '{"email":"[email protected]"}'
{"id":1,"email":"[email protected]"}
Here validateBody is application-level middleware scoped to exactly one method and path. The GET /users route shares the path but a different verb, so the validator is bypassed for reads.
Ordering effects
Express walks the middleware stack top to bottom in the exact order you register it. A global app.use() placed after a route handler that already sent a response will never run for that route. The classic example is registering express.json() below your routes — req.body ends up undefined because the parser never had a chance to execute.
// WRONG: route registered before the parser
app.post("/echo", (req, res) => res.json(req.body)); // req.body is undefined
app.use(express.json());
// RIGHT: parser first, then routes
app.use(express.json());
app.post("/echo", (req, res) => res.json(req.body));
Note: In Express 5 the path-matching syntax changed — bare strings still work as prefix mounts, but patterns like
*must be named (/*splat) and the old optional-character regex rules were tightened. Plainapp.use('/path', fn)mounting behaves the same across 4.x and 5.x.
Best Practices
- Register global concerns (logging,
express.json(), timing) at the very top so every downstream handler benefits from them. - Use a path mount (
app.use('/admin', guard)) to protect a whole branch instead of repeating the same guard on each route. - Reach for
app.get/app.postwhen middleware should be tied to a specific verb; useapp.usewhen the verb is irrelevant. - Read
req.originalUrl(notreq.url) inside path-scoped middleware if you need the full incoming path. - Order is execution order — place response-sending handlers after, never before, the middleware they depend on.
- Always call
next()or end the response in every middleware to avoid hanging requests.