Middleware Fundamentals

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Express 5.3 (npm express 5.3.0, published 2026-10-09) on Node.js 18 or later, the minimum for Express 5, and it states where Express 4 (latest 4.22.3) behaves differently. Everything was checked against the official documentation on 2026-10-29. Middleware is the single most important idea in Express: every other feature in this section (routing, body parsing, sessions, error handling, security headers, static files) is a middleware function, a way of registering one, or a rule about how a chain of them runs. The examples all belong to the Bookshelf API that grows across the section, use ESM and the node: prefix for built-in modules, and contain placeholders only, never real secrets.

What middleware is

An Express application is, in the official wording, a series of middleware function calls. A middleware function is an ordinary JavaScript function that receives the request object (req), the response object (res) and a function conventionally named next. Each function can do any combination of four things:

  • run arbitrary code (logging, timing, authentication, database access);

  • change the request and response objects (parse a body, set a header, attach the authenticated user);

  • end the request-response cycle by sending a response;

  • call next() to hand the request to the next function in the chain.

Express keeps an ordered list of these functions, called the stack, and runs the request through the list from the first entry to the last. The first function that sends a response ends the cycle; the functions after it never run for that request. If a function neither responds nor calls next(), the request hangs until the client gives up, because nothing else will ever touch it.

Since Express 5 the stack is managed by the standalone router package (the 2.x line, maintained by the pillarjs organization), which Express depends on directly; in Express 4 the same code lived inside Express itself. Application objects, express.Router() instances and route objects all use the same layer machinery, which is why the rules on this page apply equally to all of them.

The (req, res, next) contract

The shape of a normal middleware function is fixed:

function middleware(req, res, next) {
  // read or change req and res, then either respond or call next()
}

The arguments are the Node.js http.IncomingMessage and http.ServerResponse objects extended by Express with its own properties and methods, plus next. Express never inspects what the function returns, with one exception described in Async middleware in Express 5: if it returns a promise, Express waits on that promise only to catch a rejection.

What a function does at the end of its work decides what happens to the request:

Call Effect Typical use

res.send(), res.json(), res.end(), res.redirect(), res.render(), res.sendFile()

Ends the cycle. No later layer runs for this request.

A route handler, an authentication failure (401), a cache hit, a rate-limit refusal (429).

next()

Passes control to the next layer in the stack that matches the request.

Logging, parsing, adding headers, loading data, authentication that succeeded.

next('route')

Skips the remaining handlers of the current route and goes on to the next matching route. Only meaningful inside a route handler registered with app.METHOD(), app.all() or router.METHOD().

A guard that decides a different route should answer the same path.

next('router')

Leaves the current router instance entirely and continues in whatever follows it in the parent stack.

A router-level guard that fails the whole router (for example "this router is for administrators only") without sending a response.

next(err) with any other truthy value

Treats the value as an error: skips every normal layer and jumps to the next error-handling middleware. The default error handler answers if none is registered.

Anything that went wrong and is not handled locally.

Nothing

The request hangs.

Always a bug.

The two strings 'route' and 'router' are special values, not errors. Any other argument other than a falsy one is an error, including a string such as 'forbidden'. Do not pass strings to next() expecting a message; pass an Error. The figure shows the four exits from a layer.

A request enters the application stack, where a logger and a JSON parser run in order, then enters a router mounted at /books, where an authentication layer runs, then a route with two handlers, loadBook and a handler that sends JSON. A plain next call moves to the next layer. A next route call skips the remaining handlers of the route, a next router call leaves the router, and a next err call or rejected promise skips all normal layers and lands in the error-handling middleware, which falls back to finalhandler

The word "onion" is a convenient picture of the nesting (application, then router, then route), not a promise that control returns outward the way it does in Koa. In Express, next() returns undefined and a middleware does not get a second turn when the downstream handlers finish. If you need to act after the response has been sent, listen for the response’s finish event, as the first example below does.

A first complete pipeline

The following program wires a logger, a JSON body parser, a path-scoped middleware, a router with a two-step route, a 404 handler and an error handler. Run it with npm install express and node pipeline.mjs.

// pipeline.mjs
import { randomUUID } from 'node:crypto';
import express from 'express';

const app = express();
const books = new Map([['1', { id: '1', title: 'Dune' }]]);

// 1. Application-level middleware without a path: runs for every request.
app.use((req, res, next) => {
  const incoming = req.get('x-request-id');
  req.id = incoming && /^[\w-]{1,64}$/.test(incoming) ? incoming : randomUUID();
  res.set('X-Request-Id', req.id);
  const started = process.hrtime.bigint();
  // Work that must happen after the response is sent goes in an event listener,
  // because next() does not return when the downstream layers finish.
  res.on('finish', () => {
    const ms = Number(process.hrtime.bigint() - started) / 1e6;
    console.log(`${req.id} ${req.method} ${req.originalUrl} ${res.statusCode} ${ms.toFixed(1)}ms`);
  });
  next();
});

// 2. Built-in middleware. Without a parser, req.body is undefined in Express 5.
app.use(express.json({ limit: '100kb' }));

// 3. Path-scoped middleware: runs for /books and everything below it.
app.use('/books', (req, res, next) => {
  console.log(`books area: baseUrl=${req.baseUrl} url=${req.url}`);
  next();
});

// 4. A router with a route made of two handlers and a fallback route.
const router = express.Router();

router.get(
  '/:id',
  (req, res, next) => {
    const book = books.get(req.params.id);
    if (!book) {
      return next('route'); // skip the second handler, try the next matching route
    }
    res.locals.book = book;
    next();
  },
  (req, res) => {
    res.json(res.locals.book);
  },
);

router.get('/:id', (req, res) => {
  res.status(404).json({ title: 'Not Found', status: 404, detail: `No book with id ${req.params.id}` });
});

app.use('/books', router);

// 5. Last normal layer: nothing matched, so answer 404 ourselves.
app.use((req, res) => {
  res.status(404).json({ title: 'Not Found', status: 404 });
});

// 6. Error-handling middleware: four parameters, registered after everything else.
app.use((err, req, res, next) => {
  if (res.headersSent) {
    return next(err); // too late to change the response, let Express close the connection
  }
  console.error(req.id, err);
  res.status(err.status ?? 500).json({ title: 'Internal Server Error', status: err.status ?? 500 });
});

app.listen(3000, (error) => {
  if (error) {
    throw error; // Express 5 passes listen errors (such as EADDRINUSE) to the callback
  }
  console.log('Bookshelf listening on http://localhost:3000');
});

Requesting /books/1 runs layers 1, 2, 3 and then the router; the first handler stores the book in res.locals, the second sends it. Requesting /books/99 makes the first handler call next('route'), which skips the second handler and lands in the fallback route. Requesting /shelves passes through layers 1 and 2, matches nothing else and reaches the 404 layer. Notice that the router is mounted on the same /books prefix as the path-scoped logger: mounting is just a layer registered with a path, so the order in which app.use() calls appear is the order in which requests are processed.

Ordering is the program

Express has no priority numbers, phases or dependency declarations. The only rule is registration order: for each request, the router walks the stack from the first registered layer to the last and runs every layer whose path and method match, until one ends the cycle. Everything else follows from that rule.

A sound order for most applications is:

  1. Cross-cutting layers that must see every request: request ID, logging, security headers (Helmet), CORS, compression.

  2. Layers that make later layers possible: body parsers, cookie parser, session, then authentication that depends on the session. See Body Parsing and Cookies & Sessions.

  3. Static files, when they should not pay for sessions or authentication.

  4. Routes and routers, from the most specific to the most general.

  5. A catch-all layer that answers 404 for anything unmatched.

  6. Error-handling middleware, last of all.

Some consequences worth memorising:

  • A layer registered after a response was sent never runs for that request. A layer registered after the route that needs it is as good as absent: a JSON parser registered after the routes leaves req.body undefined in the handlers.

  • A 404 layer must come after every route, an error handler after every layer that can fail. Registering an error handler first is a common mistake: errors raised later skip it, because Express only looks for error handlers after the layer that raised.

  • Two routes with the same path and method are both candidates: the first is tried, and the second runs only if the first calls next('route') or next() past its last handler. This is how the example above implements a fallback.

  • Express answers HEAD requests with a matching GET route and answers OPTIONS automatically for paths that have routes, unless a layer responds earlier. CORS preflights depend on the CORS layer being registered before the routes.

Kinds of middleware

The official guide groups middleware by where it is attached and who wrote it. The categories overlap, since a function is just a function; the category tells you how the stack treats it.

Kind Attached with Notes

Application-level

app.use(fn), app.use(path, fn), app.METHOD(path, fn), app.all(path, fn)

Bound to the app object. With app.use() and no path it runs for every request; with app.METHOD() it runs only when the method and path match. app.query() (Express 5.3) registers a handler for the HTTP QUERY method in the same way.

Router-level

router.use(fn), router.use(path, fn), router.METHOD(path, fn), router.route(path)

Works exactly like application-level middleware but belongs to an express.Router() instance, so a group of routes and their guards can be mounted, reused and tested as one unit. next('router') leaves it. See Routers & Modular Apps.

Error-handling

app.use((err, req, res, next) => ...)

Recognised by having exactly four declared parameters. Runs only after something called next(err) or threw. See Error-handling middleware.

Built-in

express.json(), express.urlencoded(), express.raw(), express.text(), express.static(), express.Router()

Shipped with Express. Since Express 4.16 the body parsers are re-exports of body-parser; in Express 5 they come from body-parser 2.x.

Third-party

app.use(helmet()), app.use(cors()), app.use(morgan('combined')), …​

Any npm package that returns a middleware function. The Express team maintains a set of them, covered in Official Middleware Modules; everything else is a dependency you have to judge yourself.

Two practical differences between application-level and router-level middleware matter in large applications. First, router-level middleware sees req.url relative to the mount path while req.baseUrl holds the mount path and req.originalUrl keeps the full URL, so a router can be mounted at several prefixes without changes. Second, a router is itself a middleware function: app.use('/books', router) just registers it as a layer, which is why routers nest inside routers.

Path-scoped middleware, sub-stacks and arrays

app.use() and router.use() accept an optional first argument that restricts the layer to requests whose path starts with that mount path. The comparison is by whole path segments, not raw characters, and the mount path is removed from req.url for the duration of the layer. With the path-to-regexp 8 syntax that Express 5 uses (through router 2), a mount path may be a string, a path with named parameters such as /books/:id, a regular expression, or an array of any of these. The wildcard * on its own is no longer a valid path in Express 5: use a path-less app.use(fn) to run for everything, or a named wildcard such as /{*splat} where the wildcard value is needed. The full syntax is in Route Paths & Parameters.

Every registration method accepts more than one function. All of these are equivalent ways of building a sub-stack:

// pipeline-forms.mjs
import express from 'express';

const app = express();

const logUrl = (req, res, next) => {
  console.log(`${req.method} ${req.originalUrl}`);
  next();
};
const logMethod = (req, res, next) => {
  console.log(`Method: ${req.method}`);
  next();
};
const sendOk = (req, res) => {
  res.json({ ok: true });
};

// Registered before the routes, because a route that responds ends the pipeline.
// One middleware for several mount paths.
app.use(['/a', '/b'], (req, res, next) => {
  res.set('X-Group', 'ab');
  next();
});

// A path-less layer runs for every request, whatever the method or path.
app.use((req, res, next) => {
  res.set('X-Powered-By', 'Bookshelf');
  next();
});

// Several functions, one after another.
app.get('/a', logUrl, logMethod, sendOk);

// An array of functions.
const subStack = [logUrl, logMethod];
app.get('/b', subStack, sendOk);

// Arrays and individual functions can be mixed.
app.get('/c', [logUrl], logMethod, sendOk);

app.listen(3000, (error) => {
  if (error) {
    throw error;
  }
});

Arrays make it easy to share a reusable guard stack between routes (for example [authenticate, requireRole('editor')]) without repeating it. Express flattens the arrays when the route is registered, so they have no run-time cost.

Configurable middleware factories

Most useful middleware needs configuration: which header carries the API key, which roles may pass, how long a response may be cached. The convention, used by every Express middleware package, is a factory: an outer function that takes options and returns the (req, res, next) function. express.json({ limit: '1mb' }), cors({ origin }) and morgan('tiny') are all factories.

// factories.mjs
import { timingSafeEqual } from 'node:crypto';
import express from 'express';

/**
 * Accepts a request only when the configured header carries one of the configured keys.
 * Misconfiguration is detected once, when the factory runs, not on every request.
 */
export function requireApiKey({ header = 'x-api-key', keys } = {}) {
  if (!Array.isArray(keys) || keys.length === 0) {
    throw new TypeError('requireApiKey: "keys" must be a non-empty array');
  }
  const accepted = keys.map((key) => Buffer.from(key));

  return function requireApiKeyMiddleware(req, res, next) {
    const supplied = Buffer.from(req.get(header) ?? '');
    // timingSafeEqual needs buffers of the same length, so compare lengths first.
    const ok = accepted.some((key) => key.length === supplied.length && timingSafeEqual(key, supplied));
    if (!ok) {
      return res.status(401).json({ title: 'Unauthorized', status: 401 });
    }
    next();
  };
}

/** Allows the request only when req.user has one of the listed roles. */
export function requireRole(...roles) {
  if (roles.length === 0) {
    throw new TypeError('requireRole: at least one role is required');
  }
  const allowed = new Set(roles);

  return function requireRoleMiddleware(req, res, next) {
    if (!req.user) {
      return res.status(401).json({ title: 'Unauthorized', status: 401 });
    }
    if (!allowed.has(req.user.role)) {
      return res.status(403).json({ title: 'Forbidden', status: 403 });
    }
    next();
  };
}

/** Sets Cache-Control for the responses of the layers that follow. */
export function cacheFor(seconds, { shared = true } = {}) {
  const value = `${shared ? 'public' : 'private'}, max-age=${Math.trunc(seconds)}`;
  return (req, res, next) => {
    res.set('Cache-Control', value);
    next();
  };
}

// --- usage -----------------------------------------------------------------
const app = express();
app.use(express.json());

// Stand-in for real authentication, only so that the example runs on its own.
// A real application would verify a session or token here; see the authentication page.
app.use((req, res, next) => {
  req.user = { name: 'demo', role: req.get('x-demo-role') ?? 'reader' };
  next();
});

const books = [{ id: '1', title: 'Dune' }];

app.get('/books', cacheFor(60), (req, res) => {
  res.json(books);
});

app.post('/books', requireRole('editor', 'admin'), (req, res) => {
  const book = { id: String(books.length + 1), title: String(req.body?.title ?? '') };
  books.push(book);
  res.status(201).location(`/books/${book.id}`).json(book);
});

app.delete(
  '/books/:id',
  requireApiKey({ keys: [process.env.BOOKSHELF_ADMIN_KEY ?? 'replace-with-a-generated-key'] }),
  requireRole('admin'),
  (req, res) => {
    const index = books.findIndex((book) => book.id === req.params.id);
    if (index === -1) {
      return res.status(404).json({ title: 'Not Found', status: 404 });
    }
    books.splice(index, 1);
    res.sendStatus(204);
  },
);

app.listen(3000, (error) => {
  if (error) {
    throw error;
  }
});

Design rules that make factories pleasant to use:

  • Validate the options when the factory runs, so a typo fails at startup instead of on the first request.

  • Do the expensive work (building a Set, compiling a regular expression, creating a connection pool) in the outer function and keep the inner function cheap.

  • Return a named function; stack traces and the DEBUG=router output (see Debugging & Logging) show the function’s name, and an anonymous layer is hard to find.

  • Never store per-request state in the factory’s closure. All requests share that closure; per-request state belongs on req, res.locals or a request-scoped store such as AsyncLocalStorage.

  • Keep the factory decoupled from any particular application: accept options, not the app.

Async middleware in Express 5

In Express 4, an async middleware function that threw or awaited a rejected promise produced an unhandled rejection: Express never saw the failure, the request hung, and Node.js either logged a warning or, from Node.js 15 on, terminated the process. The usual workarounds were a try/catch that called next(err) in every handler, a small wrapper around each handler, or the express-async-errors package that patched the framework.

Express 5 fixes this at the source. When a layer’s function returns a promise (that is, when it is an async function or returns any thenable), the router attaches a rejection handler, and a rejection is forwarded to next(err) automatically. A thrown exception inside an async function is a rejection, so it reaches the error handlers too. If the rejection reason is falsy, the router wraps it in an Error so that the error path still runs. This applies to route handlers, to app.use() and router.use() layers, and to router.param() callbacks alike; it does not apply to error-handling middleware itself being invoked, whose own failures are reported through the same mechanism.

// async-middleware.mjs
import { setTimeout as sleep } from 'node:timers/promises';
import express from 'express';

class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.name = 'HttpError';
    this.status = status;
  }
}

// A stand-in for a database call.
const repository = {
  async findById(id) {
    await sleep(10);
    if (id === 'boom') {
      throw new Error('database connection lost');
    }
    return id === '1' ? { id: '1', title: 'Dune', author: 'Frank Herbert' } : null;
  },
};

const app = express();

// An async middleware: no try/catch, no wrapper.
async function loadBook(req, res, next) {
  const book = await repository.findById(req.params.id); // a rejection becomes next(err)
  if (!book) {
    throw new HttpError(404, `Book ${req.params.id} not found`); // so does a throw
  }
  res.locals.book = book;
  next(); // do not forget it: returning from an async function is not the same as calling next()
}

app.get('/books/:id', loadBook, (req, res) => {
  res.json(res.locals.book);
});

app.use((err, req, res, next) => {
  if (res.headersSent) {
    return next(err);
  }
  const status = err instanceof HttpError ? err.status : 500;
  res.status(status).json({
    title: status === 500 ? 'Internal Server Error' : err.message,
    status,
  });
});

app.listen(3000, (error) => {
  if (error) {
    throw error;
  }
});

The new behaviour has limits that the official guide spells out and that cause real bugs:

  • Only a returned promise is watched. Code that runs later and outside that promise (a callback passed to setTimeout, an event listener, a callback-style library) is not covered: an exception thrown there is an uncaught exception and a next(err) call is the only way to report an error. Wrap callback APIs with util.promisify() or the promise versions in node:fs/promises and node:timers/promises, so that the middleware can await them.

  • Resolving is not responding. An async middleware that finishes without sending a response and without calling next() leaves the request hanging, exactly like a synchronous one.

  • Calling next() and then continuing to await other work means that work runs after the downstream handlers have started. If that later work fails, the error arrives when a response may already be on its way, and the error handler must cope with res.headersSent.

  • Awaiting next() gains nothing: next() returns undefined, not a promise for the downstream layers.

  • async functions passed to app.use() under Express 4 are not protected. Code that has to run on both major lines needs the old wrapper until it is upgraded; see Migrating & What Changed.

Error handling in depth, including the default handler and Problem Details responses, is the subject of Error Handling.

Error-handling middleware

Error-handling middleware is defined like any other middleware but with four parameters, (err, req, res, next). Express decides whether a function is an error handler by its declared length, so the fourth parameter must be present even if the function never uses it. Two consequences follow:

  • An arrow function with default values or a rest parameter can report a different length and silently stop being an error handler. Keep the signature plain.

  • Express skips error handlers while processing normally and skips normal layers while processing an error. Calling next(err) from an error handler passes the error to the next one; calling next() with no argument resumes the normal stack, which is rarely what you want.

When no error handler takes the error, the built-in finalhandler answers: it uses err.status or err.statusCode when they are between 400 and 599 (otherwise 500), copies err.headers and, outside production, includes the stack trace in the HTML body. If headers were already sent, it closes the connection instead. Always define your own handler that returns the format your clients expect, and set NODE_ENV=production in production so that stack traces stay out of responses.

Sharing data between layers with res.locals

Middleware often produces a value that later layers or the view need: the authenticated user, the loaded book, the request ID, the current language. There are three places to put it:

Place Lifetime and scope Use it for

res.locals

One request. Merged into the template variables of res.render() automatically. The Express docs describe it as the object that scopes request-level variables to views.

Data for the view or for later layers of the same request: loaded entities, flash messages, CSRF tokens, locale.

req.<property>

One request. Not visible to templates unless copied.

Properties that other middleware by convention expects on req (req.user from Passport, req.session, req.id).

app.locals

The whole application lifetime, shared by every request and every render.

Constants for templates, such as a site name or helper functions. Never per-user data.

res.locals is an object created per response, so values cannot leak between concurrent requests, unlike a variable declared at module level. Prefer it to inventing properties on req when the data has no established name; plain properties on req work but collide easily with the framework’s own (req.query, req.body, req.params, req.path) and with other middleware. In TypeScript, res.locals has the type Record<string, any> & Locals, which can be narrowed through the Locals interface or a generic parameter (see TypeScript with Express).

The view-locals example: data for templates

The official view-locals example in the expressjs/express repository shows three ways of getting data into a view: middleware that loads data and stores it for the route, a route that builds its locals itself, and values passed straight to res.render(). The Bookshelf version below follows the same ideas and uses the EJS template engine (npm install express ejs). Create the two files shown and run node view-locals.mjs.

// view-locals.mjs
import { fileURLToPath } from 'node:url';
import express from 'express';

const app = express();

app.set('view engine', 'ejs');
app.set('views', fileURLToPath(new URL('./views', import.meta.url)));

// app.locals: lives as long as the application and is visible in every template.
app.locals.siteName = 'Bookshelf';

// A stand-in for a database.
const library = {
  async countBooks() {
    return 2;
  },
  async listBooks() {
    return [
      { id: '1', title: 'Dune' },
      { id: '2', title: 'Neuromancer' },
    ];
  },
};

// Middleware that prepares per-request data for the view.
async function loadCount(req, res, next) {
  res.locals.count = await library.countBooks();
  next();
}

async function loadBooks(req, res, next) {
  res.locals.books = await library.listBooks();
  next();
}

// Approach 1: middleware fills res.locals, the route only chooses the template.
app.get('/', loadCount, loadBooks, (req, res) => {
  res.render('index', { title: 'All books' });
});

// Approach 2: the route computes its own values and passes them to render().
app.get('/favourites', async (req, res) => {
  const books = (await library.listBooks()).slice(0, 1);
  res.render('index', { title: 'Favourites', count: books.length, books });
});

app.use((err, req, res, next) => {
  if (res.headersSent) {
    return next(err);
  }
  res.status(500).type('text/plain').send('Something went wrong');
});

app.listen(3000, (error) => {
  if (error) {
    throw error;
  }
});
<!-- views/index.ejs -->
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title><%= title %> | <%= siteName %></title>
</head>
<body>
  <h1><%= title %></h1>
  <p><%= count %> book(s)</p>
  <ul>
    <% books.forEach((book) => { %>
      <li><%= book.title %></li>
    <% }); %>
  </ul>
</body>
</html>

When res.render() builds the variable set it merges, from lowest to highest priority, app.locals, res.locals and the object passed to render(); on a name clash the object passed to render() wins. Express 5 always renders asynchronously (Express 4 could render synchronously with some engines, which caused subtle ordering bugs), and an error during rendering is passed to next(err) when no callback is supplied. The <%= %> tag HTML-escapes its value; use the unescaped tag only for markup you built yourself. Views and engines are covered in Template Engines & Views.

What changed recently

Version Middleware-relevant change

Express 5.0.0 (2024-09)

Promises returned by handlers and middleware are forwarded to next(err) on rejection. Routing moved to router 2.x and path-to-regexp 8, so *, optional :param? and in-string regular expressions no longer work as before. req.body is undefined until a parser runs, express.urlencoded() defaults to extended: false, req.query is a getter backed by the simple parser by default, app.router is available again, and res.render() is always asynchronous.

Express 5.1.0 (2025-03-31)

First 5.x release tagged latest on npm; Express 5 is what npm install express installs. The minimum Node.js is 18.

Express 5.2 (2025-12-01) and 5.3 (2026-10-09)

Security and dependency updates (for example qs and body-parser fixes), plus app.query() for the HTTP QUERY method. Check History.md in the repository before upgrading.

The migration guide lists every removed method and the codemods that rewrite them; this page covers only what affects how middleware is written. The Express team has not announced a version 6, so nothing here depends on one.

Common bugs and how to recognise them

Symptom Usual cause Fix

The request hangs, the browser spins until timeout.

A layer neither responded nor called next(), including an async middleware that returns without calling it, or a conditional branch that forgets the else.

Make every code path end with a response, next() or next(err). Add a request timeout at the reverse proxy.

ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent

The layer responded and then called next() (or responded twice), most often if (err) res.send(err) without a return, or next(err) followed by more code.

return immediately after sending or calling next. One response per request.

req.body is undefined (Express 5)

No body parser was registered, it was registered after the route, or the content type does not match the parser.

Register express.json() before the routes; check Content-Type. Express 4 also left req.body undefined with no parser; it was {} only when a registered parser ran but skipped the request.

req.user is undefined although the user is logged in

Passport (or any code reading the session) was registered before the session middleware, or before passport.session().

Register in this order: cookie and session middleware, passport.initialize(), passport.session(), then routes.

CORS errors only on preflight

cors() is registered after a layer or route that answers OPTIONS, or only on some routes.

Register cors() before the routes, or add an explicit OPTIONS handler with the same options.

A custom error handler is never called

It is registered before the failing layer, it declares three parameters, or the error happens in a callback outside any promise.

Register it last with four parameters; forward callback errors with next(err).

The 404 handler answers valid URLs

The catch-all layer was registered before the routes.

Register the 404 layer after every route and router.

TypeError: Cannot set property query of #<IncomingMessage> which has only a getter

Middleware (a sanitiser, for instance) assigns to req.query, which is a read-only getter in Express 5.

Write the sanitised copy to another property or res.locals, or define an own property with Object.defineProperty.

Data from one user appears in another user’s response

Per-request state was kept in a module-level variable or in a factory’s closure, which every request shares.

Keep per-request state in res.locals, on req or in AsyncLocalStorage.

Missing parameter name at index 1: * at startup

An Express 4 catch-all such as app.use('*', fn) or app.get('*', fn) under Express 5.

Use a path-less app.use(fn) or the named wildcard /{*splat}.

The process crashes with an uncaught exception

A throw inside a timer, an event listener or a callback, outside any returned promise.

Catch it there and call next(err), or convert the API to promises.

Express 4: an async middleware that rejects leaves the request hanging

Express 4 ignores the returned promise.

Upgrade to Express 5, or wrap the handler so rejections reach next.

A habit that prevents most of these: when adding a layer, write down which later layers depend on it and which earlier layers it depends on, then place it accordingly. A short test that exercises the application with supertest and checks req-dependent behaviour in the order the layers run catches ordering errors long before production; see Testing.

Where the books are outdated

The four books consulted for this section target Express 4.0 to 4.18 and Node.js 0.10 to 20. Their middleware chapters remain a good picture of the concepts, but several practices have since changed:

Older practice Where it appears Current behaviour

Mounting the whole bodyParser() function with app.use() to parse JSON and forms at once.

Monteiro 2014, chapter 3

The combined function was dropped when Express 4 stopped bundling Connect middleware. Use express.json() and express.urlencoded(), built into Express since 4.16.0 and backed by body-parser 2.x in Express 5.

Installing the standalone body-parser package as a separate dependency for every project.

All four books

Still works, but redundant: the built-ins are the same code. In Express 5, req.body is undefined without a parser.

Calling res.send(err) for a failed callback without returning, so the handler continues and responds twice.

Sharma, chapter 2; Monteiro 2014

Returns ERR_HTTP_HEADERS_SENT. Use return next(err) and a central error handler; Express 5 forwards rejected promises on its own.

A try/catch around the body of every async route handler to call next(err).

Bugl, chapters 3 and 6

Not needed in Express 5, where a rejection or throw in an async function reaches the error handlers.

app.use('*', ...) as a catch-all, for example for a single-page application fallback.

Bugl, chapters 7 and 8; Sharma, chapter 6

Throws at startup in Express 5 because of path-to-regexp 8. Use a path-less app.use(fn) or /{*splat}.

Registering middleware before session() that needs req.session, or Passport before the session.

Monteiro 2014, chapter 4

Ordering rules are unchanged: layers run in registration order, so session middleware must come first.

res.send(200, body) and other status-first call signatures.

Sharma, chapter 2

Removed in Express 5. Chain res.status(200).send(body).

Callback-based data loading inside middleware, nested several levels deep.

Older tutorials in general

async/await middleware with no wrapper, with the limits described in Async middleware in Express 5.