Middleware
In an API, middleware handles everything that is the same for many endpoints: tracing, content-type rules, caching headers, authentication. Writing them as small configurable factories keeps them reusable.
On this page · 6 min · Beginner
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Beginner
- Updated
- Oct 28, 2026
Related tutorials
Interview questions
Learning objectives#
- Write middleware factories that take options
- Add request ids for tracing across services
- Enforce JSON with 415 Unsupported Media Type
- Scope middleware to the app, a path, or one route
Explanation#
A middleware factory is a function returning a middleware: const cacheFor = seconds => (req, res, next) => { ... }. Options are captured once; the returned function runs per request.
Request ids: reuse an incoming X-Request-Id (from a gateway or front end) or generate one, attach it to req and echo it in the response. Logs and error reports then link every line of one request together.
Content-type checks: if a POST/PUT/PATCH is not application/json, return 415 instead of silently treating the body as empty.
Scope deliberately: app.use(fn) for everything, app.use("/api", fn) for a path, app.get(path, fn, handler) for one route.
Many middleware already exist: helmet, cors, express-rate-limit, compression. Write your own for project-specific rules.
Syntax#
const cacheFor = seconds => (req, res, next) => {
res.set('Cache-Control', `public, max-age=${seconds}`);
next();
};
app.get('/api/courses', cacheFor(60), controller.list);
Code example: Three reusable middleware factories#
import express from 'express';
import { randomUUID } from 'node:crypto';
const requestId = ({ header = 'X-Request-Id' } = {}) => (req, res, next) => {
req.id = req.get(header) ?? randomUUID();
res.set(header, req.id);
next();
};
const requireJson = () => (req, res, next) => {
if (['POST', 'PUT', 'PATCH'].includes(req.method) && !req.is('application/json')) {
return res.status(415).json({ error: 'Content-Type must be application/json' });
}
next();
};
const cacheFor = seconds => (req, res, next) => {
res.set('Cache-Control', `public, max-age=${seconds}`);
next();
};
const app = express();
app.use(requestId()); // 1. every request
app.use('/api', requireJson(), express.json()); // 2. only under /api
app.get('/api/courses', cacheFor(60), (req, res) => res.json([{ id: 1, title: 'Node.js REST APIs' }])); // 3. one route
app.post('/api/courses', (req, res) => res.status(201).json({ id: 2, ...req.body }));
const server = app.listen(3104, async () => {
const base = 'http://localhost:3104/api/courses';
const list = await fetch(base, { headers: { 'X-Request-Id': 'abc-123' } });
console.log('GET', list.status, '| id:', list.headers.get('x-request-id'), '| cache:', list.headers.get('cache-control'));
const text = await fetch(base, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: 'title=Express' });
console.log('POST text/plain', text.status, await text.text());
const json = await fetch(base, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Express Middleware' }) });
console.log('POST json', json.status, await json.text(), '| generated id length:', json.headers.get('x-request-id').length);
server.close();
});
GET 200 | id: abc-123 | cache: public, max-age=60
POST text/plain 415 {"error":"Content-Type must be application/json"}
POST json 201 {"id":2,"title":"Express Middleware"} | generated id length: 36This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- requestId reuses the client’s X-Request-Id or generates a UUID, and echoes it back.
- requireJson and express.json run only for paths under /api.
- cacheFor(60) adds Cache-Control to the course list only.
- A text/plain POST gets 415 instead of an empty body reaching the handler.
- The JSON POST succeeds and gets a fresh 36-character request id.
Real-world use case#
A support agent copies the X-Request-Id from a failed request in the browser’s network tab; the developer searches logs for that id and sees every log line from the gateway, the API and the database layer for that one request.
Common mistakes#
Code outside the returned function runs once. Per-request values (time, ids) must be computed inside it.
Running expensive checks (DB lookups) for /health or static files wastes resources. Scope it.
Form posts reaching a JSON API produce empty req.body and confusing 400s. Return 415.
Best practices#
- Factories for configurable middleware
- Request ids on every request and log line
- 415 for wrong content types
- Scope middleware to where it is needed
Remember this#
Factory: options outside, (req, res, next) inside.
X-Request-Id: reuse or generate, then echo.
Wrong Content-Type → 415.
Practice exercise#
Your task: Write a deprecated(sunsetDate) middleware factory that adds the headers Deprecation: true and Sunset: <date> to responses, and apply it to GET /api/v1/old-courses.
Need a hint?
Show solution
import express from 'express';
const deprecated = sunsetDate => (req, res, next) => {
res.set({ Deprecation: 'true', Sunset: new Date(sunsetDate).toUTCString() });
next();
};
const app = express();
app.get('/api/v1/old-courses', deprecated('2027-01-31'), (req, res) => res.json([]));
const server = app.listen(3153, async () => {
const r = await fetch('http://localhost:3153/api/v1/old-courses');
console.log(r.headers.get('deprecation'), '|', r.headers.get('sunset')); // true | Sun, 31 Jan 2027 00:00:00 GMT
server.close();
});
Mini quiz#
Which status code means “I don’t accept this Content-Type”?
Want more? Practise all Node.js REST API MCQs · Node.js REST API interview questions
My notes#
Saved in this browser.