Express Middleware

Understand Express's middleware pipeline: write functions that run before your routes, call next() to continue, attach data to req, stop requests early, apply middleware to the whole app, one router or one route, and use popular packages like cors and helmet.

Lesson 12 of 43Intermediate17 min read

Many things need to happen for most requests, not just one route: logging each request, parsing JSON bodies, checking that the user is signed in, adding security headers, measuring response times. Writing that code inside every route handler would be repetitive and easy to forget. Express handles it with middleware.

A middleware function sits between the incoming request and your route handler. It receives req, res and a function called next. It can read or change the request, add information to it, send a response itself (for example, "401 Unauthorized"), or call next() to pass control to the next function in line. An Express app is really a pipeline of middleware, and route handlers are simply the last step.

You've already used middleware: express.json() is one, and so are routers. In this lesson you'll write your own logging, timing and authentication middleware, control where it applies, build configurable middleware, and add well-known packages such as cors and helmet. The final kind, error-handling middleware, gets its own lesson.

How middleware works

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

app.use(logger);                   // runs for every request
app.get('/', (req, res) => res.send('Home'));
  • Middleware runs in the order it's added with app.use, before any route defined after it.
  • Each middleware must either call next() or send a response. If it does neither, the request hangs.
  • Middleware can attach data for later steps, such as req.user or req.startTime.
  • next() passes control on; next(err) skips to the error-handling middleware.

Where middleware applies

How it's addedRuns for
app.use(fn)Every request
app.use('/admin', fn)Requests whose path starts with /admin
router.use(fn)Every request handled by that router
app.get('/path', fn, handler)Only that route
app.post('/path', [validate, auth], handler)That route, through several steps in order

Applying middleware as narrowly as possible keeps the app fast and makes the rules easy to see: an authentication check on the admin router protects every admin route at once.

Node.js Web Syntax

Middleware patterns

const timing = (req, res, next) => { req.start = Date.now(); next(); };
app.use(timing);
app.use('/api', apiLimiter);
router.use(requireLogin);
app.get('/admin', requireLogin, requireRole('admin'), showAdmin);
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(cors({ origin: 'https://shop.example' }));
app.use(helmet());
res.on('finish', () => { /* runs after the response is sent */ });

Node.js Web Summary

  • Middleware has the signature (req, res, next) and runs in the order it's added.
  • Each middleware must call next() or send a response.
  • app.use applies to everything; paths, routers and route arguments narrow it down.
  • Factory functions create configurable middleware such as requireRole('admin').
  • Packages like cors, helmet, morgan and express-rate-limit add common features.

Node.js Web Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Logging and timing every request

Node.js
import express from 'express';

const app = express();
const log = [];

app.use((req, res, next) => {
  const start = performance.now();
  res.on('finish', () => {
    const ms = performance.now() - start;
    log.push(`${req.method} ${req.originalUrl} ${res.statusCode} ${ms < 1000 ? 'fast' : 'slow'}`);
  });
  next();
});

app.use((req, res, next) => {
  req.requestId = `req-${log.length + 1}`;
  next();
});

app.get('/products', (req, res) => res.json({ id: req.requestId, items: 2 }));

const server = app.listen(0, async () => {
  const base = `http://localhost:${server.address().port}`;
  console.log(await (await fetch(`${base}/products`)).text());
  await fetch(`${base}/missing`);
  console.log(log.join('\n'));
  server.close();
});
Output
{"id":"req-1","items":2}
GET /products 200 fast
GET /missing 404 fast
Line-by-line explanation
  1. Line 6app.use with no path runs for every request, including ones that end in 404.
  2. Line 8The finish event fires after the response is sent, so the status code is known.
  3. Line 12next passes the request on to the next middleware.
  4. Lines 15 to 16Middleware can attach data to req for later handlers to use.
  5. Line 20The route handler reads the value the middleware added.
Example 2

An authentication check on a router

Node.js
import express, { Router } from 'express';

const users = { 'token-ada': { name: 'Ada', role: 'admin' }, 'token-ben': { name: 'Ben', role: 'customer' } };

function requireLogin(req, res, next) {
  const token = req.get('Authorization')?.replace('Bearer ', '');
  const user = users[token];
  if (!user) return res.status(401).json({ error: 'Please sign in' });
  req.user = user;
  next();
}

function requireRole(role) {
  return (req, res, next) => {
    if (req.user.role !== role) return res.status(403).json({ error: 'Admins only' });
    next();
  };
}

const admin = Router();
admin.use(requireLogin);
admin.get('/dashboard', (req, res) => res.json({ hello: req.user.name }));
admin.delete('/products/:id', requireRole('admin'), (req, res) => res.sendStatus(204));

const app = express();
app.use('/admin', admin);

const server = app.listen(0, async () => {
  const base = `http://localhost:${server.address().port}/admin`;
  const call = (path, token, method = 'GET') =>
    fetch(base + path, { method, headers: token ? { Authorization: `Bearer ${token}` } : {} });
  console.log((await call('/dashboard')).status);
  console.log(await (await call('/dashboard', 'token-ben')).text());
  console.log((await call('/products/3', 'token-ben', 'DELETE')).status);
  console.log((await call('/products/3', 'token-ada', 'DELETE')).status);
  server.close();
});
Output
401
{"hello":"Ben"}
403
204
Line-by-line explanation
  1. Line 6req.get reads a request header, here the bearer token.
  2. Line 8Without a valid token, the middleware answers 401 and never calls next.
  3. Line 9The signed-in user is attached to req for later middleware and routes.
  4. Lines 13 to 18A factory returns middleware configured with the required role.
  5. Line 21router.use protects every route in the admin router with one line.
  6. Line 23Route-level middleware adds an extra check to just one route.
Example 3

Built-in and third-party middleware

Node.js
import express from 'express';

const app = express();
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));
app.use((req, res, next) => {
  res.set('X-Content-Type-Options', 'nosniff');
  res.set('Access-Control-Allow-Origin', 'https://shop.example');
  next();
});

app.post('/feedback', (req, res) => {
  res.json({ received: req.body, type: req.is('json') ? 'json' : 'form' });
});

const server = app.listen(0, async () => {
  const url = `http://localhost:${server.address().port}/feedback`;
  let r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{"rating":5}' });
  console.log(await r.text());
  console.log(r.headers.get('x-content-type-options'), r.headers.get('access-control-allow-origin'));
  r = await fetch(url, { method: 'POST', body: new URLSearchParams({ rating: '4', comment: 'Quick delivery' }) });
  console.log(await r.text());
  r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'x'.repeat(20000) }) });
  console.log(r.status);
  server.close();
});
Output
{"received":{"rating":5},"type":"json"}
nosniff https://shop.example
{"received":{"rating":"4","comment":"Quick delivery"},"type":"form"}
413
Line-by-line explanation
  1. Line 4express.json parses JSON bodies and rejects any larger than 10 KB.
  2. Line 5express.urlencoded parses HTML form submissions into req.body.
  3. Lines 6 to 10Hand-written middleware sets headers; in real apps, the helmet and cors packages do this with sensible defaults.
  4. Line 13req.is checks the request's Content-Type.
  5. Line 21Form values arrive as strings.
  6. Lines 23 to 24A body over the limit is refused with 413 Payload Too Large.

Common Node.js Web Mistakes

Middleware that never calls next

Don't
app.use((req, res, next) => {
  console.log(req.url);           // the request hangs forever
});
Do
app.use((req, res, next) => {
  console.log(req.url);
  next();
});

Calling next after sending a response

Don't
function auth(req, res, next) {
  if (!req.user) res.status(401).send('Sign in');
  next();                         // the route runs anyway and tries to respond again
}
Do
function auth(req, res, next) {
  if (!req.user) return res.status(401).send('Sign in');
  next();
}

Adding middleware after the routes

Don't
app.post('/orders', createOrder);
app.use(express.json());          // too late: req.body is undefined in createOrder
Do
app.use(express.json());
app.post('/orders', createOrder);

Node.js Web in Real Projects

Middleware in production APIs

A typical Express API starts with helmet(), cors() with a list of allowed origins, a request logger such as pino-http, express.json() with a size limit and a rate limiter. Routers for signed-in features use authentication middleware, admin routes add role checks, and validation middleware checks each request body before it reaches the handler. The pipeline ends with a 404 handler and an error handler.

Node.js Web FAQ

What is the difference between app.use and app.get in Express?

app.use registers middleware that runs for every HTTP method and for every path that starts with the given prefix (or for all paths if you leave the prefix out). app.get registers a handler only for GET requests to a specific path. Use app.use for things that apply broadly, such as logging and body parsing, and method functions for individual endpoints.

How do I pass data from middleware to a route handler?

Attach it to the request object, for example req.user = user, and later handlers read req.user. Express also provides res.locals, an object that lives for one request and is automatically available to view templates. Choose clear property names so they don't clash with properties Express already uses.

Can Express middleware be an async function?

Yes. In Express 5, if an async middleware throws or its promise rejects, Express passes the error to your error-handling middleware just as if you had called next(err). You still need to call next() yourself when the middleware succeeds; returning from the function isn't enough.

How do I enable CORS in Express?

Install the cors package and add it as middleware: app.use(cors({ origin: 'https://app.example' })). Listing the allowed origins is safer than calling cors() with no options, which allows any site. You can also apply it to a single router if only part of your API is meant to be called from other domains.

Why does my Express middleware run twice for one page load?

Often it doesn't: the browser makes a second request, usually for /favicon.ico, and your logger shows both. Log req.url to check. If the same URL really appears twice, look for the middleware being registered twice, for example with both app.use and inside a router mounted on the same path.

Node.js Web Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the middleware so the request continues to the route.

app.use((req, res, next) => {
  req.startedAt = Date.now();
  ();
});
Multiple choiceeasy

What are the three parameters of normal Express middleware?

Multiple choicemedium

What happens if middleware neither calls next() nor sends a response?

Predict the outputmedium

In which order are the messages printed for GET /?

app.use((req, res, next) => { console.log('A'); next(); });
app.get('/', (req, res) => { console.log('C'); res.end(); });
app.use((req, res, next) => { console.log('B'); next(); });
Multiple choicehard

Why write requireRole('admin') as a function that returns middleware?

Node.js Web Quiz

5 questions to check what you learned.

  1. Which line applies middleware to every request?

    • app.get(fn)
    • app.use(fn)
    • app.all(fn)
    • app.listen(fn)
  2. What does the cors package do?

    • Lets browsers on other domains call your API
    • Compresses responses
    • Logs requests
    • Parses cookies
  3. Which status should authentication middleware send when no valid login is present?

    • 403
    • 404
    • 500
    • 401
  4. How do you apply middleware to just one route?

    • app.use only
    • Put it in package.json
    • It's not possible
    • Pass it before the handler: app.get('/x', check, handler)
  5. How can middleware run code after the response has been sent?

    • By calling next twice
    • By using setTimeout
    • By listening for the response's finish event
    • It can't