Middleware

Node.js · Lesson 15 of 30 7 min read Updated

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:

  1. Pass the request on by calling next(), usually after reading or adding something to req.
  2. End the request by sending a response, for example a 401 error, and not calling next().

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.

JavaScript
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:

Output
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 adds req.user, a body parser adds req.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

CodeRuns 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.

JavaScript
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:

Output
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:

Output
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 calls next() straight away.
  • The API key check ends the request with 401 Unauthorized (no credentials) or 403 Forbidden (credentials not accepted). The return stops the function so next() is not called after a response.
  • Only /api/orders uses the check, because it is passed to that route alone. /api/status stays 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 into req.body. Accepts an option such as { limit: "100kb" } (the default).
  • express.urlencoded(): parses HTML form submissions (name=Meera&city=Kochi) into req.body.
  • express.static("public"): serves files from a folder.
  • express.text() and express.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():

JavaScript
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 forms

A request now gets security headers from helmet, and the CORS header that lets the browser page at https://shop.example.in call your API:

Output
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.

PackageJob
helmetSets many security-related HTTP headers
corsAllows chosen websites to call your API from the browser
express-rate-limitLimits how many requests one client can make
multerHandles file uploads (see File Uploads)
express-sessionServer-side sessions and login state
morgan or pino-httpRequest 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 call next() or send a response.
  • Calling next() after sending a response. Later handlers then try to send again: ERR_HTTP_HEADERS_SENT. Use return res... to stop.
  • Wrong order. A route added before express.json() gets req.body === undefined; a logger added after the routes never sees requests the routes answered.
  • Calling the middleware instead of passing it. app.use(logger()) runs logger immediately. Write app.use(logger), unless it is a factory such as requireApiKey(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 as req.user or req.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 async middleware 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
JavaScript
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);
Output
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?

2. In what order does Express run middleware added with app.use()?

3. How does a login middleware usually make the user available to route handlers?

4. Which status code fits a request that sent no credentials at all?

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