Types of Middleware
Every middleware function in Express has the same signature, but where you attach it and how Express invokes it determine its behavior. The Express documentation formally recognizes five categories of middleware: application-level, router-level, built-in, third-party, and error-handling. Knowing which category you reach for keeps your request pipeline predictable and your concerns cleanly separated. This page categorizes all five and shows a concrete, runnable example of each.
Application-level middleware
Application-level middleware is bound directly to an instance of the app object via app.use() or a routing method such as app.get(). It runs for every matching request that flows through the app, in the order it was registered.
const express = require("express");
const app = express();
// Runs for every request, regardless of path or method
app.use((req, res, next) => {
req.requestTime = Date.now();
console.log(`${req.method} ${req.url}`);
next();
});
// Scoped to a path: only runs for requests starting with /users
app.use("/users", (req, res, next) => {
console.log("Inside the /users branch");
next();
});
app.get("/", (req, res) => {
res.send(`Handled at ${req.requestTime}`);
});
app.listen(3000, () => console.log("Listening on http://localhost:3000"));
Output:
$ curl http://localhost:3000/
GET /
Handled at 1749859200000
The first argument to app.use() is an optional mount path. Omit it and the middleware runs for all requests; supply one and Express only invokes it when the request path matches that prefix.
Router-level middleware
Router-level middleware works identically to application-level middleware, except it is bound to an instance of express.Router() instead of the app. This scopes the middleware to just the routes defined on that router, which is ideal for concerns tied to a single resource.
const express = require("express");
const router = express.Router();
// Runs before every route in this router only
router.use((req, res, next) => {
console.log(`[router] ${req.method} ${req.originalUrl}`);
next();
});
router.get("/", (req, res) => res.json({ ok: true }));
module.exports = router;
Mount it on the app, and the middleware fires only for requests that reach this router:
app.use("/api", router); // router middleware runs for /api/*
Built-in middleware
Built-in middleware ships with Express itself — no extra packages required. Since Express 4.16, the most common parsers and a static file server are bundled directly into the framework. There are three:
| Middleware | Purpose |
|---|---|
express.json() | Parses incoming requests with JSON payloads into req.body |
express.urlencoded() | Parses URL-encoded form bodies into req.body |
express.static() | Serves static assets (HTML, CSS, images) from a directory |
const express = require("express");
const app = express();
app.use(express.json()); // populate req.body from JSON
app.use(express.urlencoded({ extended: true })); // populate req.body from forms
app.use(express.static("public")); // serve files in ./public
app.post("/echo", (req, res) => {
res.json({ received: req.body });
});
app.listen(3000);
Output:
$ curl -X POST http://localhost:3000/echo \
-H "Content-Type: application/json" \
-d '{"name":"Ada"}'
{"received":{"name":"Ada"}}
Tip: Order matters.
express.json()must run before any route handler that readsreq.body, otherwisereq.bodywill beundefined. Register your parsers near the top of the app.
Third-party middleware
Third-party middleware is published as standalone npm packages and added to the request pipeline with app.use() after installation. It covers the cross-cutting concerns Express deliberately leaves out of core, such as logging, CORS, sessions, and cookie parsing.
npm install morgan cors
const express = require("express");
const morgan = require("morgan");
const cors = require("cors");
const app = express();
app.use(morgan("dev")); // HTTP request logging
app.use(cors()); // enable Cross-Origin Resource Sharing
app.get("/", (req, res) => res.send("Hello"));
app.listen(3000);
Output:
GET / 200 2.041 ms - 5
Functionally a third-party middleware is just an application- or router-level middleware whose code you didn’t write — it is attached the same way and obeys the same ordering rules.
Error-handling middleware
Error-handling middleware is the one category with a different signature: it takes four arguments — (err, req, res, next). Express recognizes the function by its arity and only calls it when an error is passed to next(err) or, in Express 5, when an async handler rejects. It must be defined last, after all other middleware and routes.
app.get("/boom", (req, res, next) => {
next(new Error("Something failed"));
});
// Error handler — note the four parameters
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: err.message });
});
Output:
$ curl http://localhost:3000/boom
{"error":"Something failed"}
Note: In Express 4 you must call
next(err)manually from inside an async handler’scatchblock. Express 5 forwards rejected promises to the error handler automatically, so a barethrowinside anasyncroute reaches this middleware without explicit wiring.
Best Practices
- Register built-in parsers (
express.json(),express.urlencoded()) before any route that readsreq.body. - Scope concerns to a router with router-level middleware instead of guarding paths globally at the app level.
- Place error-handling middleware last, after every route and router, so it can catch errors from all of them.
- Keep the four-argument signature on error handlers even if you ignore
next— Express identifies them by arity. - Prefer well-maintained third-party middleware (cors, morgan, helmet) over hand-rolling solved cross-cutting concerns.
- Always call
next()(or send a response) in middleware; forgetting to do so leaves the request hanging.