Middleware
Many things must happen for almost every request: log it, parse its body, check who is calling, add security headers. Writing that code inside every route would be repetitive and easy to forget. Express solves this with middleware: functions that run one after another for each request, before (or instead of) your route handler.
Middleware is the heart of Express. Body parsing, static files, logins, error handling and file uploads are all middleware. In this lesson you will see how the chain works, write your own logger and access check, and plug in popular middleware packages.
What middleware is
A middleware function receives three arguments: req, res and next. It can do one of two things:
- Pass the request on by calling
next(), usually after reading or adding something toreq. - End the request by sending a response, for example a
401error, and not callingnext().
Think of a railway station: the ticket checker, then the security scanner, then the platform. Each one can let you through or stop you. Your route handler is the train at the end.
import express from "express";
const app = express();
app.use((req, res, next) => {
console.log("1. first middleware");
next();
});
app.use((req, res, next) => {
console.log("2. second middleware");
req.requestTime = new Date().toISOString().slice(0, 10);
next();
});
app.get("/hello", (req, res) => {
console.log("3. route handler");
res.send(`Hello! Today is ${req.requestTime}`);
});
app.listen(3000);A request to /hello prints this in the server terminal:
1. first middleware 2. second middleware 3. route handler
and the browser receives Hello! Today is 2026-10-30. Two things to notice:
- Middleware runs in the order it is added. Express keeps one list of middleware and routes and walks through it from top to bottom.
- The second middleware added a property to
req, and the route handler read it. This is how middleware shares data with later code: a login check addsreq.user, a body parser addsreq.body.
If a middleware neither calls next() nor sends a response, the request hangs until the browser gives up. That is the most common middleware bug.
Where middleware can be attached
| Code | Runs for |
|---|---|
app.use(fn) | Every request |
app.use("/admin", fn) | Every request whose path starts with /admin |
app.get("/orders", fn, handler) | Only this route; fn runs before handler |
app.get("/orders", [fn1, fn2], handler) | An array of middleware for one route |
router.use(fn) | Every request handled by that router |
Writing useful middleware
Here are two real-world middleware functions. The logger measures how long each request takes. The API key check is written as a factory: a function that takes settings and returns a middleware, so you can reuse it with different keys.
import express from "express";
const app = express();
// Logger: runs for every request
function logger(req, res, next) {
const start = performance.now();
res.on("finish", () => {
const ms = (performance.now() - start).toFixed(1);
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${ms} ms`);
});
next();
}
// Factory: returns a middleware that checks an API key
function requireApiKey(validKeys) {
return (req, res, next) => {
const key = req.get("X-API-Key");
if (!key) return res.status(401).json({ error: "API key missing" });
const client = validKeys[key];
if (!client) return res.status(403).json({ error: "API key not valid" });
req.client = client; // share data with later handlers
next();
};
}
const keys = { "k-123": "Rohtak Sweets", "k-456": "Pune Books" };
app.use(logger);
app.use(express.json());
app.get("/api/status", (req, res) => res.json({ status: "up" }));
app.get("/api/orders", requireApiKey(keys), (req, res) => {
res.json({ client: req.client, orders: 12 });
});
app.listen(3000);Calling it four times, with no key, a wrong key and a valid key, gives these responses:
200 {"status":"up"}
401 {"error":"API key missing"}
403 {"error":"API key not valid"}
200 {"client":"Rohtak Sweets","orders":12}The logger prints one line per request in the server terminal:
GET /api/status 200 48.8 ms GET /api/orders 401 4.1 ms GET /api/orders 403 82.7 ms GET /api/orders 200 56.3 ms
How it works:
- The logger cannot know the status code when the request arrives, so it listens for the response's
"finish"event, which fires after the response is sent. It still callsnext()straight away. - The API key check ends the request with
401 Unauthorized(no credentials) or403 Forbidden(credentials not accepted). Thereturnstops the function sonext()is not called after a response. - Only
/api/ordersuses the check, because it is passed to that route alone./api/statusstays public.
In a real app the keys would come from a database or environment variables, not from the code. You will build proper logins in Password Hashing & Sessions and JWT Authentication.
Built-in middleware
Express 5 includes these:
express.json(): parses JSON bodies intoreq.body. Accepts an option such as{ limit: "100kb" }(the default).express.urlencoded(): parses HTML form submissions (name=Meera&city=Kochi) intoreq.body.express.static("public"): serves files from a folder.express.text()andexpress.raw(): read the body as a string or a Buffer.
Third-party middleware
The npm community publishes middleware for most common jobs. Install them like any package and add them with app.use():
import express from "express";
import cors from "cors";
import helmet from "helmet";
const app = express();
app.use(helmet()); // security headers
app.use(cors({ origin: "https://shop.example.in" })); // allow this front end to call the API
app.use(express.urlencoded()); // HTML formsA request now gets security headers from helmet, and the CORS header that lets the browser page at https://shop.example.in call your API:
x-powered-by: null x-content-type-options: nosniff x-frame-options: SAMEORIGIN access-control-allow-origin: https://shop.example.in
Without helmet, Express adds X-Powered-By: Express to every response, which tells attackers what you are running; helmet removes it. CORS (Cross-Origin Resource Sharing) is a browser rule: a web page may only call an API on another domain if that API allows it. The cors middleware sends the permission header. You will meet both again in Security Best Practices.
| Package | Job |
|---|---|
helmet | Sets many security-related HTTP headers |
cors | Allows chosen websites to call your API from the browser |
express-rate-limit | Limits how many requests one client can make |
multer | Handles file uploads (see File Uploads) |
express-session | Server-side sessions and login state |
morgan or pino-http | Request logging |
Async middleware in Express 5
Middleware often needs to await something, such as looking up a user in a database. In Express 5 you can write async (req, res, next) => { ... }, and if the promise rejects, Express passes the error to your error handler. In Express 4 the same code left the request hanging, so you had to wrap it in try...catch and call next(err) yourself.
Common mistakes
- Forgetting
next(). The request hangs. Every middleware must callnext()or send a response. - Calling
next()after sending a response. Later handlers then try to send again:ERR_HTTP_HEADERS_SENT. Usereturn res...to stop. - Wrong order. A route added before
express.json()getsreq.body === undefined; a logger added after the routes never sees requests the routes answered. - Calling the middleware instead of passing it.
app.use(logger())runsloggerimmediately. Writeapp.use(logger), unless it is a factory such asrequireApiKey(keys)that returns the real middleware. - Doing slow work in global middleware. It runs for every request, including images and CSS files.
Key points
- Middleware is a function
(req, res, next)that runs before route handlers, in the order it is added. - It either calls
next()to continue or sends a response to stop the chain. - Add data for later code by setting properties on
req, such asreq.userorreq.client. - Attach middleware globally with
app.use(), to a path prefix, or to single routes. - Factories are functions that take options and return middleware.
- Express 5 passes errors from
asyncmiddleware to the error handler automatically.
Exercise
Write a middleware factory onlyDuring(startHour, endHour) for a school canteen API. If the current hour (from new Date().getHours()) is outside the range, it responds with status 403 and { "error": "Canteen is closed" }. Otherwise it calls next(). Use it only on POST /api/orders, and keep GET /api/menu open all day.
Show solution
import express from "express";
function onlyDuring(startHour, endHour) {
return (req, res, next) => {
const hour = new Date().getHours();
if (hour < startHour || hour >= endHour) {
return res.status(403).json({ error: "Canteen is closed" });
}
next();
};
}
const app = express();
app.use(express.json());
app.get("/api/menu", (req, res) => {
res.json(["Poha", "Idli", "Veg sandwich"]);
});
app.post("/api/orders", onlyDuring(8, 15), (req, res) => {
res.status(201).json({ ordered: req.body.item });
});
app.listen(3000);403 {"error":"Canteen is closed"}The output shows a POST /api/orders sent at 3:30 pm, just after the 8 am to 3 pm window closed (hour is 15, and 15 >= 15). Because the middleware is passed only to the orders route, the menu stays available all day.
Quick Quiz
1. What must a middleware function do if it does not send a response?
Calling next() passes control to the next middleware or route; otherwise the request hangs.
2. In what order does Express run middleware added with app.use()?
Express walks its list of middleware and routes from top to bottom.
3. How does a login middleware usually make the user available to route handlers?
req is shared by every function that handles the request, so it carries data along the chain.
4. Which status code fits a request that sent no credentials at all?
401 means authentication is missing or failed; 403 means the caller is known but not allowed.
Learn Node.js with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
