Node.js REST API · Foundations

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.

Beginner 6 min Lesson 6 of 25
0/25 in Node.js REST API
On this page · 6 min · Beginner

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#

JavaScript Illustrative
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#

JavaScriptapi-middleware.js Illustrative
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();
});
Expected output
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: 36

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. requestId reuses the client’s X-Request-Id or generates a UUID, and echoes it back.
  2. requireJson and express.json run only for paths under /api.
  3. cacheFor(60) adds Cache-Control to the course list only.
  4. A text/plain POST gets 415 instead of an empty body reaching the handler.
  5. 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#

Doing work at factory time that should be per request

Code outside the returned function runs once. Per-request values (time, ids) must be computed inside it.

Global middleware that only some routes need

Running expensive checks (DB lookups) for /health or static files wastes resources. Scope it.

Accepting any content type

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?
res.set accepts an object: res.set({ Deprecation: "true", Sunset: ... }).
Show solution
JavaScript Illustrative
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#

Middleware quizMultiple choice Question 1 of 3

Which status code means “I don’t accept this Content-Type”?

Score so far: 0/0

Want more? Practise all Node.js REST API MCQs · Node.js REST API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.