What Is Middleware?
Middleware is the single most important concept in Express. Almost everything the framework does — parsing JSON bodies, serving static files, logging requests, checking authentication, handling errors — is implemented as middleware. Understanding what a middleware function is, and how control flows from one to the next, is the key that unlocks the rest of the framework.
Middleware is just a function
A middleware function is a plain function that receives three arguments: the request object (req), the response object (res), and a function conventionally named next. It sits in the path between the incoming HTTP request and the response your app sends back, with full access to read and modify both.
function logger(req, res, next) {
console.log(`${req.method} ${req.url}`);
next();
}
That signature — (req, res, next) — is the defining shape of middleware. A middleware function can do three things with the cycle: inspect or mutate req/res, end the request by sending a response, or call next() to hand control to the next function in line.
The request-response cycle
Every request that reaches Express enters a cycle that must end in exactly one response. Express does not send anything on your behalf; some function in the chain must either write a response or pass control along until one does. If a request enters the pipeline and no function ever responds or calls next(), the request simply hangs until the client times out.
const express = require("express");
const app = express();
app.use((req, res, next) => {
req.receivedAt = new Date().toISOString();
next(); // pass control onward
});
app.get("/", (req, res) => {
res.json({ message: "Hello", at: req.receivedAt }); // ends the cycle
});
app.listen(3000, () => console.log("Listening on http://localhost:3000"));
Output:
$ curl http://localhost:3000/
{"message":"Hello","at":"2026-06-14T10:21:05.482Z"}
The first function annotates req and steps aside; the route handler reads that annotation and ends the cycle with res.json(). Because the data attached to req survives across functions, middleware is the idiomatic way to share computed values (the current user, a request ID, a parsed body) with everything downstream.
Calling next() vs. ending the response
The two outcomes of a middleware function are mutually exclusive, and confusing them is the most common Express bug.
| Action | What it does | When to use it |
|---|---|---|
next() | Passes control to the next matching middleware | When this function only does part of the work |
next(err) | Skips ahead to error-handling middleware | When something went wrong |
res.send() / res.json() / res.end() | Ends the cycle and replies to the client | When this function produces the final response |
A middleware should do one of these. If you both send a response and call next(), Express continues into the chain and a later handler may try to respond again — producing the dreaded Error: Cannot set headers after they are sent to the client.
app.use((req, res, next) => {
if (!req.headers.authorization) {
return res.status(401).json({ error: "Unauthorized" }); // end here
}
next(); // otherwise continue
});
Warning: Always
returnwhen you send a response from a branch. Forgetting thereturnlets execution fall through tonext(), causing the handler to both reply and continue — the classic “headers already sent” crash.
The pipeline is ordered
Middleware runs in the exact order it is registered. Express keeps a stack of functions and walks it top to bottom for each request; next() advances to the next entry. This ordering is not cosmetic — it determines behavior. A body parser must be registered before any route that reads req.body, and a logger placed first will see every request while one placed last will miss responses that short-circuited earlier.
const express = require("express");
const app = express();
app.use(express.json()); // 1. parse JSON bodies
app.use((req, res, next) => { // 2. log after parsing
console.log("Body:", req.body);
next();
});
app.post("/echo", (req, res) => { // 3. respond
res.json(req.body);
});
app.listen(3000);
Send a request and watch the stages fire in sequence:
Output:
$ curl -X POST http://localhost:3000/echo \
-H "Content-Type: application/json" -d '{"name":"Ada"}'
{"name":"Ada"}
# server console:
Body: { name: 'Ada' }
If you swapped the order and put the logger before express.json(), req.body would be undefined because no parser had run yet. The pipeline is a conveyor belt: each function depends on the work of the ones before it.
Note: In Express 5, an
asyncmiddleware that throws or rejects is automatically caught and forwarded to your error handler. In Express 4 you must catch errors yourself and callnext(err), or the rejection goes unhandled.
Middleware with a path
app.use() can take an optional path as its first argument. The middleware then runs only when the request URL begins with that path, making it easy to scope behavior to a subtree of your API.
// Runs for every request
app.use(express.json());
// Runs only for requests starting with /admin
app.use("/admin", (req, res, next) => {
console.log("Admin area accessed");
next();
});
Without a path, middleware applies to all requests. This same mechanism is how routers, static file servers, and route handlers are all mounted — they are middleware too, just specialized kinds.
Best Practices
- Treat middleware as your composition unit: factor cross-cutting concerns (auth, logging, validation) into small, single-purpose functions.
- Always either end the response or call
next()— never both — andreturnafter sending from a conditional branch. - Register middleware in dependency order; put body parsers and request-context setup before the routes that rely on them.
- Use
next(err)(or throw inside anasynchandler on Express 5) to route failures to a dedicated error-handling middleware rather than responding inline. - Attach shared values to
req(e.g.req.user,req.requestId) so downstream handlers can consume them without re-computing. - Scope middleware with a mount path when it only applies to part of the app, instead of guarding every request with conditionals.