Skip to content
Express.js ex middleware 4 min read

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:

MiddlewarePurpose
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 reads req.body, otherwise req.body will be undefined. 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’s catch block. Express 5 forwards rejected promises to the error handler automatically, so a bare throw inside an async route reaches this middleware without explicit wiring.

Best Practices

  • Register built-in parsers (express.json(), express.urlencoded()) before any route that reads req.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.
Last updated June 14, 2026
Was this helpful?