Express Middleware
Reviewed & published by Brayan K
Middleware is a function that runs between the incoming request and your final response, with the signature (req, res, next), letting you log, parse, authenticate, or transform every request in a pipeline.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll write your own middleware, understand why order matters, share data down the chain with req, use built-in parsers like express.json(), and add a catch-all error handler.
What You'll Learn in This Lesson
- What middleware is and the (req, res, next) signature
- Why you must call next() or send a response
- How middleware order forms a request pipeline
- Sharing data down the chain by attaching to req
- Using built-in middleware like express.json()
- Adding error-handling middleware with four arguments
1️⃣ What Middleware Is
A middleware function takes three arguments: req (the request), res (the response), and next (a function that hands control to whatever comes next). Register it with app.use() and it runs for every request. The golden rule: always either call next() or send a response.
// MIDDLEWARE is a function that runs IN THE MIDDLE — between the incoming
// request and your final response. Its signature is always (req, res, next).
const express = require("express");
const app = express();
// app.use registers middleware that runs for EVERY request.
// It receives the request (req), the response (res), and next().
app.use((req, res, next) => {
console.log("Request received:", req.method, req.url);
// You MUST call next() to pass control to the next handler.
// Forget it, and the request hangs forever.
next();
});
app.get("/", (req, res) => {
res.send("Hello from the route handler!");
});
app.listen(3000, () => console.log("Server on http://localhost:3000"));
// When a browser requests GET /, the middleware logs the line FIRST,
// calls next(), and then the route handler sends the response.The middleware logged the request, then called next() to let the route handler send the actual reply. Drop the next() and the browser would spin forever — the request would never reach the handler.
2️⃣ Order Matters: a Pipeline
Middleware runs in registration order, like a pipeline. Each function can attach data to req so later functions — including your route handler — can read it. This is how things like the logged-in user or a request timer get shared down the chain.
// Middleware runs IN THE ORDER you register it. Think of it as a pipeline:
// each function can read/modify req and res, then pass the baton with next().
const express = require("express");
const app = express();
// 1) A timing logger — records when the request started.
app.use((req, res, next) => {
req.startTime = Date.now(); // attach data to req for later middleware
console.log("1. Logger: " + req.method + " " + req.url);
next();
});
// 2) A fake "auth" check — adds a user, then continues.
app.use((req, res, next) => {
req.user = { name: "Sam" }; // share data down the chain
console.log("2. Auth: attached user " + req.user.name);
next();
});
// 3) The route handler can read everything the middleware added.
app.get("/", (req, res) => {
const ms = Date.now() - req.startTime;
res.send("Hi " + req.user.name + " (handled in " + ms + "ms)");
});
app.listen(3000);The logger ran first, then auth, then the handler — exactly the order they were registered. Because each stored data on req, the final handler could read both req.startTime and req.user.
3️⃣ Built-in and Error-Handling Middleware
Express bundles useful middleware. express.json() parses a JSON request body onto req.body. And a special error-handling middleware — recognized by its four arguments (err, req, res, next) — catches anything that throws, so you can return a clean error instead of crashing.
// Express ships with BUILT-IN middleware. express.json() parses an incoming
// JSON body and puts the result on req.body — without it, req.body is undefined.
const express = require("express");
const app = express();
app.use(express.json()); // built-in body parser for JSON
app.post("/echo", (req, res) => {
// Thanks to express.json(), req.body is a real object here.
res.json({ youSent: req.body });
});
// ERROR-HANDLING middleware has FOUR arguments: (err, req, res, next).
// Express recognizes it by the four-argument signature and calls it
// whenever a handler throws or passes an error to next(err).
app.get("/boom", (req, res) => {
throw new Error("Something broke");
});
app.use((err, req, res, next) => {
console.error("Caught:", err.message);
res.status(500).json({ error: err.message });
});
app.listen(3000);
// POST /echo with body {"hi":1} -> {"youSent":{"hi":1}}
// GET /boom -> 500 {"error":"Something broke"}Your turn. Fill in the two ___ blanks to complete a logger and a timestamp middleware, then run it.
// 🎯 YOUR TURN — finish the middleware, then run: node app.js
const express = require("express");
const app = express();
// TODO 1: write a middleware that logs the request method and url,
// then calls next(). Remember the (req, res, next) signature.
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
___; // 👉 pass control to the next handler
});
// TODO 2: add a middleware that puts the current time on req.now,
// then continues the chain.
app.use((req, res, next) => {
req.now = ___; // 👉 a Date or timestamp
next();
});
app.get("/", (req, res) => {
res.send("Request handled at " + req.now);
});
app.listen(3000, () => console.log("Listening on 3000"));
// ✅ Expected log when you visit GET / :
// GET /Mini-Challenge: An API Key Gate
Write a middleware that only lets the request through when ?key=secret is present, and rejects everything else with a 401. Register it before your routes.
// 🎯 MINI-CHALLENGE: a tiny "API key" gate.
// Save as gate.js and run: node gate.js
//
// 1. Write a middleware checkKey(req, res, next) that looks at
// req.query.key (the ?key=... part of the URL).
// 2. If the key equals "secret", call next() to allow the request.
// 3. Otherwise respond res.status(401).json({ error: "no access" })
// and do NOT call next().
// 4. Register it with app.use(checkKey) BEFORE your routes.
//
// ✅ Expected:
// GET /data?key=secret -> 200 {"ok":true}
// GET /data -> 401 {"error":"no access"}
const express = require("express");
const app = express();
// your code here
app.get("/data", (req, res) => res.json({ ok: true }));
app.listen(3000);Common Errors (and the fix)
- The request hangs forever — A middleware neither called next() nor sent a response. Make sure every path does one or the other.
- req.body is undefined — You forgot app.use(express.json()) before the route, so the JSON body was never parsed.
- Error handler never runs — Error-handling middleware must have all four arguments (err, req, res, next) and be registered after your routes.
- "Cannot set headers after they are sent" — You sent a response and then called next() or sent again. End the cycle once; don't continue after responding.
- Middleware runs in the wrong order — Express respects registration order. Put parsers and auth above the routes that depend on them.
Pro Tips
- 💡 Scope middleware to a path: app.use("/admin", checkAdmin) runs only for URLs starting with /admin.
- 💡 Pass errors forward: in async middleware, use next(err) to route a failure to your error handler.
- 💡 One error handler at the end: keep a single four-argument handler last so it catches everything.
📋 Quick Reference — Express Middleware
| Goal | Syntax |
|---|---|
| Global middleware | app.use((req, res, next) => {}) |
| Pass control on | next() |
| Parse JSON body | app.use(express.json()) |
| Path-scoped | app.use("/api", mw) |
| Send an error onward | next(err) |
| Error handler | (err, req, res, next) => {} |
🎉 Lesson Complete!
- ✅ Middleware is a function with the signature (req, res, next)
- ✅ Always call next() or send a response — never both, never neither
- ✅ Middleware runs in registration order, forming a pipeline
- ✅ Attach data to req to share it down the chain
- ✅ express.json() parses JSON bodies onto req.body
- ✅ Error handlers take four arguments and go last
Practice quiz
What is the standard signature of an Express middleware function?
- (req, res, next)
- (request)
- (next, req)
- (err, data)
Answer: (req, res, next). Middleware receives the request, the response, and next().
What must you call to pass control to the next handler in the chain?
- continue()
- next()
- pass()
- done()
Answer: next(). Calling next() hands control to the following middleware or route.
What happens if a middleware forgets to call next() (and never responds)?
- Express auto-calls next
- It skips to the route
- It logs a warning only
- The request hangs forever
Answer: The request hangs forever. Without next() or a response, the request never completes and hangs.
Which method registers middleware that runs for EVERY request?
- app.get()
- app.route()
- app.use()
- app.all()
Answer: app.use(). app.use() registers middleware that runs on every incoming request.
In what order does middleware run?
- In the order it is registered
- Alphabetically
- Randomly
- Reverse of registration
Answer: In the order it is registered. Middleware executes as a pipeline, in registration order.
Which built-in middleware parses an incoming JSON request body?
- express.text()
- express.json()
- express.raw()
- express.body()
Answer: express.json(). express.json() parses JSON bodies and puts the result on req.body.
Without express.json(), what is req.body for a JSON POST?
- An empty array
- The raw string
- undefined
- An error object
Answer: undefined. Without a body parser, req.body is undefined.
How does Express recognize ERROR-handling middleware?
- By a special name
- By a decorator
- By a config flag
- By its four arguments (err, req, res, next)
Answer: By its four arguments (err, req, res, next). The four-argument signature (err, req, res, next) marks it as error middleware.
How can one middleware share data with later handlers?
- By attaching it to the req object
- Using a global variable only
- It cannot share data
- By returning it
Answer: By attaching it to the req object. Attaching properties to req (e.g. req.user) passes data down the chain.
Which status code does the example error handler send with res.status(...)?
- 200
- 500
- 404
- 201
Answer: 500. The error handler responds with res.status(500) for a server error.
Continue this course
- Previous: Express Intro & Routing
- Next: REST APIs with Express — Design RESTful endpoints with proper verbs, status codes, and JSON
- Quick reference: Node.js cheat sheet › Express Routing & Middleware
Frequently asked questions
What exactly is middleware in Express?
Middleware is simply a function that sits in the middle of the request–response cycle. Every middleware function receives three arguments: req (the request), res (the response), and next (a function that passes control to the next middleware). Inside it you can read or modify the request and response, run side effects like logging or authentication, and then either call next() to continue or end the cycle by sending a response. Express processes middleware in the order you register them, forming a pipeline each request flows through.
Why does my request hang and never respond?
Almost always because a middleware forgot to call next() and also did not send a response. Express has no way to know you are done, so it waits forever. Every middleware must do exactly one of two things: call next() to pass control along, or end the cycle by calling res.send(), res.json(), res.end(), etc. If a middleware does asynchronous work, make sure next() is called after that work completes, not before — and watch out for code paths (like an early return) that skip it.
How is error-handling middleware different from regular middleware?
Error-handling middleware takes four arguments instead of three: (err, req, res, next). Express specifically looks at the number of parameters to recognize it, so the four-argument shape is required. It only runs when an error is thrown in a synchronous handler or passed explicitly with next(err). You typically register one error handler last, after all your routes, so it acts as a catch-all that logs the problem and sends a clean error response instead of leaking a stack trace to the client.
What is the difference between app.use and app.get?
app.use registers middleware that runs for requests of any HTTP method, and (if you give no path) for every path — it is for cross-cutting concerns like logging, parsing bodies, or auth. app.get (and app.post, app.put, etc.) registers a handler for one specific method and path, and is where you produce the actual response for that route. Under the hood a route handler is just middleware too; the difference is that app.get is scoped to a method and URL, while app.use is broad.