Skip to content
Laviyx Learn

Try

Node.js and Express course menu

Express Middleware Explained: next(), Order and Examples

Understand Express middleware: functions that run between the request and the response, calling next(), application-level and route-level middleware, built-in middleware such as express.json, writing logging, timing and API-key middleware, attaching data to req, and why order matters.

Intermediate8 min readUpdated
On this page

Middleware is the heart of Express. A middleware function receives the request and response, does something (logs, parses the body, checks a login, adds headers), and then either ends the response or calls next() to pass control to the next function in line. Route handlers are simply the last middleware in the chain.

Text
request → [ express.json ] → [ logger ] → [ auth check ] → [ route handler ] → response

Writing middleware#

logger.js (JavaScript)
import express from "express";

const app = express();

function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next();                                   // continue to the next middleware or route
}

app.use(logger);                            // runs for every request

app.get("/", (req, res) => res.send("Home"));
app.get("/about", (req, res) => res.send("About us"));

app.listen(3000, () => console.log("Ready"));
Terminal
node logger.js
Output
Ready
Terminal
curl -s localhost:3000/
curl -s localhost:3000/about
Output
Home
About us

Meanwhile, the terminal running the server shows:

Server terminal
Ready
GET /
GET /about

Each request was logged before its route ran.

A middleware function must do one of two things: send a response, or call next(). If it does neither, the request hangs.

Order matters#

Middleware runs in the order it is registered. Anything registered after a route that sends a response never runs for that route:

order.js (JavaScript)
import express from "express";

const app = express();

app.use((req, res, next) => {
  req.startedAt = Date.now();               // attach data for later middleware
  res.setHeader("X-Shop", "ShopKart");
  next();
});

app.get("/api/time", (req, res) => {
  res.json({ handledIn: Date.now() - req.startedAt < 1000 ? "under a second" : "slow" });
});

app.use((req, res) => {
  res.status(404).json({ error: `No route for ${req.method} ${req.path}` });   // runs only if nothing matched
});

app.listen(3000);
Terminal
curl -s -i localhost:3000/api/time | grep -E "^(HTTP|X-Shop)|handledIn"
curl -s localhost:3000/api/nothing-here
Output
HTTP/1.1 200 OK
X-Shop: ShopKart
{"handledIn":"under a second"}
{"error":"No route for GET /api/nothing-here"}

Two patterns from this example:

  • Middleware can attach data to req (req.startedAt, and later req.user after a login check) for the next handlers.
  • A middleware at the end with no path acts as a catch-all 404 handler, because it only runs when no route responded.

Middleware for some routes only#

Pass middleware to a specific route, or use it on a path prefix:

apikey.js (JavaScript)
import express from "express";

const app = express();
const API_KEY = "demo-key-123";            // in a real app: process.env.API_KEY

function requireApiKey(req, res, next) {
  if (req.get("x-api-key") !== API_KEY) {
    return res.status(401).json({ error: "Missing or invalid API key" });
  }
  next();
}

app.get("/api/public/menu", (req, res) => res.json(["Masala dosa", "Idli", "Filter coffee"]));
app.get("/api/admin/sales", requireApiKey, (req, res) => res.json({ today: 18450 }));

app.listen(3000);
Terminal
curl -s localhost:3000/api/public/menu
curl -s localhost:3000/api/admin/sales
curl -s -H "x-api-key: demo-key-123" localhost:3000/api/admin/sales
Output
["Masala dosa","Idli","Filter coffee"]
{"error":"Missing or invalid API key"}
{"today":18450}

req.get(name) reads a request header (case-insensitively). Route-level middleware is how authentication protects only some routes. With a router, router.use(requireApiKey) protects every route in it.

Built-in and common middleware#

MiddlewarePurpose
express.json()parse JSON bodies into req.body
express.urlencoded({ extended: true })parse HTML form submissions
express.static("public")serve static files (lesson)
cors (package)allow browsers on other origins to call the API
helmet (package)set security-related headers
morgan (package)request logging

They are all installed the same way: app.use(middleware).

Middleware that runs after the response#

To measure how long requests take, listen for the response's finish event:

timing.js (JavaScript)
import express from "express";

const app = express();

app.use((req, res, next) => {
  const start = process.hrtime.bigint();
  res.on("finish", () => {
    const ms = Number(process.hrtime.bigint() - start) / 1e6;
    console.log(`${req.method} ${req.originalUrl} → ${res.statusCode} ${ms < 100 ? "(fast)" : "(slow)"}`);
  });
  next();
});

app.get("/api/ok", (req, res) => res.json({ ok: true }));
app.get("/api/missing", (req, res) => res.status(404).json({ error: "Not here" }));

app.listen(3000);
Terminal
curl -s localhost:3000/api/ok
curl -s localhost:3000/api/missing
Output
{"ok":true}
{"error":"Not here"}
Server terminal
GET /api/ok → 200 (fast)
GET /api/missing → 404 (fast)

Real logs record the exact duration; this example prints "fast" or "slow" so its output stays the same every run.

Common mistakes#

Practice#

Practice exercise

Write middleware requireJson that rejects POST requests whose Content-Type is not JSON with status 415 and { "error": "Send JSON" }, and apply it to POST /api/feedback, which echoes back the received body.

Show hint

req.is("application/json") returns a truthy value when the request's Content-Type is JSON.

Show solution
feedback.js (JavaScript)
import express from "express";

const app = express();
app.use(express.json());

function requireJson(req, res, next) {
  if (!req.is("application/json")) return res.status(415).json({ error: "Send JSON" });
  next();
}

app.post("/api/feedback", requireJson, (req, res) => res.status(201).json({ received: req.body }));

app.listen(3000);
Terminal
curl -s -X POST localhost:3000/api/feedback -H "Content-Type: application/json" -d '{"rating":5}'
curl -s -X POST localhost:3000/api/feedback -d "rating=5"
Output
{"received":{"rating":5}}
{"error":"Send JSON"}

curl's -d without a header sends application/x-www-form-urlencoded, so the second request is rejected.

Key takeaway#

Next: Request Bodies and Validation.

Finished this lesson?