Migrating & What’s Changed

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.0 (published 2026-10-09) and Express 4.22.3 (2026-09-14, the newest 4.x), running on Node.js 24 LTS, and the facts were checked on 2026-10-29. Express 5 requires Node.js 18 or newer; Express 4 still runs on very old Node.js releases, which is one reason many projects have not moved. npm install express has installed Express 5 since 5.1.0 on 2025-03-31, so a project that was created before that date and is installed again without a lock file can already be on the new line without anyone deciding to migrate.

The page has five jobs. It lists every Express 5 breaking change from the official migration guide, each with a before and after example and the codemod that rewrites it, when one exists. It describes how to run the codemods and gives a checklist for the upgrade, in the order that keeps the application working at every step. It reminds you of the Express 3 to 4 changes that still show up in old code. It lists the deprecated middleware. And it collects, in one place, every statement from the four reference books of this section that no longer holds, with the version that changed it and a link to the page that treats the topic in depth.

The running example is the Bookshelf API used throughout the section: a small catalogue of books and reviews. The sibling pages are Versions, Support & LTS (which Express line to run and for how long), Route Paths & Parameters (the new path syntax in depth) and Security Best Practices.

How big is the migration

Express 5 is mostly a cleanup of the 4.x API, not a new framework. The changes fall in four groups:

  • Methods and properties that were deprecated for years and are now gone. Calling them throws a TypeError or silently does something different.

  • A new routing engine. The router moved to router 2 and path-to-regexp 8, which changed the spelling of wildcards, optional segments and regular expressions in route strings, and the shape of req.params.

  • Changed defaults and changed return values: req.body, req.query, req.host, express.urlencoded, express.static, res.clearCookie, res.status, res.vary and app.listen.

  • Improvements that need no migration: rejected promises reach the error handlers, res.render() is always asynchronous, and request bodies compressed with Brotli are decompressed.

The two changes that cause most of the real-world breakage are the path syntax (any catch-all such as app.get('*', ...) stops the application at start-up) and req.body being undefined when no parser ran. Both fail loudly and early in a test run, which is why the checklist later in the page puts a test run before every change.

The Express 5 breaking changes

The sections below follow the order of the official migration guide: removed methods and properties first, then changed behaviour, then the improvements. Each example is a before and after pair. Where a codemod covers the change, its name is given; the codemods are collected in Codemods.

Removed methods and properties

These calls fail on Express 5. A test that exercises the route finds them immediately.

app.del() is app.delete()

app.del() was a deprecated alias that existed because delete was a reserved word in very old JavaScript. Use app.delete(). Codemod: route-del-to-delete.

// Express 4
app.del('/books/:id', removeBook);

// Express 5
app.delete('/books/:id', removeBook);

app.param(fn) and router.param(fn)

The form of app.param() and router.param() that took only a function, to customise how parameter callbacks are registered, was deprecated since 4.11.0 and is removed. The form with a name and a function still works. Separately, a leading colon in the name (app.param(':id', fn)) is silently ignored in Express 5, so pass the plain name.

// Express 4: global customisation, removed
app.param((name, fn) => fn);

// Express 4 and 5: name and function
app.param('bookId', async (req, res, next, id) => {
  req.book = await books.find(id);
  next();
});

The singular req.accepts methods

req.acceptsCharset(), req.acceptsEncoding() and req.acceptsLanguage() are removed in favour of the plural names, which have the same meaning. Codemod: pluralize-method-names.

// Express 4
req.acceptsCharset('utf-8');
req.acceptsEncoding('br');
req.acceptsLanguage('en');

// Express 5
req.acceptsCharsets('utf-8');
req.acceptsEncodings('br');
req.acceptsLanguages('en');

req.param()

req.param(name) searched the route parameters, then the body, then the query string, which made it an injection-friendly lookup. It is removed: read the source you mean. Codemod: explicit-request-params.

// Express 4
const id = req.param('id');
const body = req.param('body');
const query = req.param('query');

// Express 5
const id = req.params.id;
const body = req.body;
const query = req.query;

Status-first call signatures

res.json(obj, status), res.jsonp(obj, status) and res.send(body, status) are removed; chain res.status() first. res.send(status) is removed too: use res.sendStatus(status), and quote a number if you really want a numeric body. Codemod: status-send-order.

// Express 4
res.json({ title: 'Dune' }, 201);
res.jsonp({ title: 'Dune' }, 201);
res.send({ title: 'Dune' }, 200);
res.send(404);

// Express 5
res.status(201).json({ title: 'Dune' });
res.status(201).jsonp({ title: 'Dune' });
res.status(200).send({ title: 'Dune' });
res.sendStatus(404);

A related trap: in Express 5, res.send(404) no longer sets a status. A number passed to res.send() is sent as a JSON body, so a leftover call produces a 200 response whose body is 404. Search for it explicitly.

res.redirect arguments and the back keyword

res.redirect(url, status) is removed; the status comes first. res.redirect('back') and res.location('back') are removed because the keyword trusted a client-controlled Referer header; redirect to an explicit URL and validate it. A redirect that relies on the default 302 is not affected. Codemods: redirect-arg-order and back-redirect-deprecated.

// Express 4
res.redirect('/books', 302);
res.redirect('back');

// Express 5
res.redirect(302, '/books');
res.redirect(req.get('Referrer') || '/');

The Referrer fallback above mirrors the official example, but a redirect to a header value is an open redirect if the value comes from another site. Where it matters, compare the header to an allow-list of your own paths before using it.

res.sendfile() and the sendFile options

res.sendfile() (lower case) is removed; use res.sendFile(). The hidden and from options of res.sendFile() are removed in favour of dotfiles and root, and the dotfile check now also applies to hidden directories anywhere in the resolved path, so a file under /var/www/app/.cache/ returns 404 unless you opt in. Codemods: camelcase-sendfile and sendfile-options.

// Express 4
res.sendfile('/path/to/cover.png');
res.sendFile(req.params.name, { hidden: true, from: '/uploads' });

// Express 5
res.sendFile('/path/to/cover.png');
res.sendFile(req.params.name, { dotfiles: 'allow', root: '/uploads' });

express.static options and express.static.mime

The hidden and from options of express.static() are removed (from was an undocumented alias of root). The express.static.mime object is removed; use the mime-types package when you need a lookup. Codemods: static-dotfiles and static-mime.

// Express 4
app.use(express.static('public', { hidden: true }));
const type = express.static.mime.lookup('json');

// Express 5
import mime from 'mime-types';
app.use(express.static('public', { dotfiles: 'allow' }));
const type = mime.lookup('json');

MIME types and debug namespaces

Two changes need no code change but can surprise a deployment. The MIME database moved on from mime-db 1.52.0, so express.static() and res.sendFile() serve some extensions with a different Content-Type; the most visible is .js, now text/javascript instead of application/javascript. The migration guide does not count this as breaking, so pin dependencies and test any client or proxy that matches on the old value.

The router’s debug namespaces were renamed because the router is now a separate module, so a DEBUG value from a runbook silently prints less.

Express 4 Express 5

express:router

router

express:router:layer

router:layer

express:router:route

router:route

express:* (includes the router)

express:*,router,router:*

# Express 4
DEBUG=express:* node index.js

# Express 5
DEBUG=express:*,router,router:* node index.js

Changed behaviour

These do not throw, so a test suite that never checks the affected behaviour passes. Read each one against your code.

Path matching with path-to-regexp 8

Route strings in app.all(), app.use(), app.METHOD() and the matching router methods are parsed by path-to-regexp 8 (through router 2). The rules that changed:

  • Wildcards must be named: /* becomes /*splat. A named wildcard does not match the root path /; use /{*splat} when it must.

  • The ? suffix is gone; optional segments use braces.

  • Regular expression syntax inside a string path is not supported; use several paths in an array, or a real RegExp object.

  • The characters ()[]?+! are reserved and must be escaped with a backslash.

  • Parameter names must be valid JavaScript identifiers, or quoted like :"this".

// Express 4
app.get('/*', notFoundPage);
app.get('/:file.:ext?', download);
app.get('/[discussion|page]/:slug', render);

// Express 5
app.get('/*splat', notFoundPage);          // not the root
app.get('/{*splat}', notFoundPage);        // including the root
app.get('/:file{.:ext}', download);
app.get(['/discussion/:slug', '/page/:slug'], render);

Source: Migrating to Express 5, "Path route matching syntax". The full grammar, escaping rules and a decision chart are in Route Paths & Parameters; the verbs and routers are in Routing and Routers & Modular Apps.

There is no codemod for the path syntax, so it needs a manual search. The patterns to grep for are '*', /*, a ? after a parameter, a parenthesis after a parameter name, and any bracket in a string path.

Rejected promises reach the error handlers

In Express 5 a middleware or handler that returns a rejected promise, or an async function that throws, calls next(err) for you. The try/catch that existed only to forward the error can go, and so can the packages that patched Express 4 to do the same.

// Express 4
app.get('/books/:id', (req, res, next) => {
  getBook(req.params.id)
    .then((book) => res.send(book))
    .catch(next);
});

// Express 5
app.get('/books/:id', async (req, res) => {
  const book = await getBook(req.params.id);
  res.send(book);
});

express.urlencoded extended defaults to false

express.urlencoded() now parses with the querystring library unless you ask for extended: true. A form whose field names use brackets (user[name]) stops producing nested objects.

// Express 4 default behaviour is now opt-in
app.use(express.urlencoded({ extended: true }));

express.static ignores dotfiles by default

The default for dotfiles is now "ignore", and the check covers hidden directories, so /.well-known/ is a 404. Serve it from its own mount with dotfiles: 'allow'.

app.use('/.well-known', express.static('public/.well-known', { dotfiles: 'allow' }));
app.use(express.static('public'));

router.param() rejects arrays

router.param(name, fn) throws a TypeError when name is not a string. app.param() still accepts an array.

// Express 4
router.param(['id', 'page'], loadParam);

// Express 5
router.param('id', loadParam);
router.param('page', loadParam);

app.listen passes errors to its callback

A failure to listen (for example EADDRINUSE) used to be an unhandled error event on the server. In Express 5 the callback receives it as an argument.

const server = app.listen(3000, '0.0.0.0', (error) => {
  if (error) {
    throw error;
  }
  console.log(`Listening on ${JSON.stringify(server.address())}`);
});

app.router is back

app.router existed in Express 3, was removed in 4, and returns in 5 as a plain reference to the application’s base router. There is still no reason to call app.use(app.router).

// Express 4
app.router; // throws Error: 'app.router' is deprecated!

// Express 5
console.log(app.router.stack.length); // the base router, created on first access

req.body is undefined without a parser

Express 4 returned {} for a request that no body parser had touched. Express 5 returns undefined, so destructuring it throws.

// Express 4: {}   Express 5: undefined
console.dir(req.body);

// defensive form
const { title, isbn } = req.body ?? {};

req.host keeps the port

Express 4 stripped the port from req.host. Express 5 returns the value of the Host header including the port. Use req.hostname when you need the host name alone, and build absolute links from configuration rather than from a header.

// GET / with Host: books.example.com:3000
// Express 4: req.host === 'books.example.com'
// Express 5: req.host === 'books.example.com:3000'
const name = req.hostname; // 'books.example.com' in both

req.params has a null prototype, array wildcards and omitted parameters

For a string path, req.params is an object with a null prototype (a regular expression path still gets a normal object), so req.params.hasOwnProperty(...) fails; use Object.hasOwn(). A wildcard produces an array of path segments, not one string. A parameter that did not match is omitted instead of being '' or undefined.

app.get('/*splat', (req, res) => {
  // GET /library/fiction
  // Express 4 with '/*':   { '0': 'library/fiction' }
  // Express 5 with '/*splat': [Object: null prototype] { splat: [ 'library', 'fiction' ] }
  res.json(req.params.splat);
});

req.query is a getter and uses the simple parser

req.query is no longer a writable property, so req.query.page = 1 has no effect. The default query parser setting changed from extended to simple, so ?filter[genre]=fiction is a flat key, not a nested object. Opt in with app.set('query parser', 'extended') only when you need nesting, and validate what you receive: nested objects are how operator injection reaches a database.

app.set('query parser', 'extended'); // opt back in, then validate the result
app.get('/search', (req, res) => {
  const page = Number(req.query.page ?? 1); // read, never assign
  res.json({ page });
});

res.clearCookie ignores maxAge and expires

res.clearCookie(name, options) no longer honours maxAge and expires; the cookie is cleared with an expiry in the past. Pass the same path, domain, secure and sameSite as when you set it.

// Express 4
res.clearCookie('session', { maxAge: 0, expires: new Date(0) });

// Express 5
res.clearCookie('session');

res.status accepts only 100 to 999

res.status() throws for anything that is not an integer between 100 and 999. A computed status that can be NaN, a string or 0 now fails at the call.

// Express 4: accepted, with a deprecation warning for the string
res.status('404').send('Not found');

// Express 5
res.status('404');                 // throws TypeError: not an integer
res.status(1000);                  // throws RangeError: outside 100 to 999
res.status(404).send('Not found'); // correct

res.vary needs a field

res.vary() with no argument only logged a warning in Express 4. It now throws.

res.vary();         // throws in Express 5
res.vary('Accept'); // correct

Improvements that need no migration

  • res.render() is always asynchronous, whatever the view engine does. Code that assumed a synchronous render for a particular engine must use the callback or the following middleware. See Template Engines & Views.

  • express.json(), express.urlencoded(), express.text() and express.raw() decompress Content-Encoding: br request bodies, in addition to gzip and deflate.

A worked migration of the Bookshelf API

The two files below are the same small service. The first is how the books teach it: Express 4, CommonJS, the separate body-parser package, status-first calls and a catch-all. The second is the Express 5 version.

// app.js, Express 4 style
const express = require('express');
const bodyParser = require('body-parser');

const books = new Map();
const app = express();

app.use(bodyParser.json());

app.get('/books/:id?', (req, res) => {
  const id = req.param('id');
  if (!id) return res.json([...books.values()]);
  const book = books.get(id);
  if (!book) return res.send(404);
  res.json(book);
});

app.post('/books', (req, res) => {
  const book = { id: String(books.size + 1), ...req.body };
  books.set(book.id, book);
  res.json(book, 201);
});

app.del('/books/:id', (req, res) => {
  books.delete(req.params.id);
  res.send(204);
});

app.get('*', (req, res) => {
  res.send({ error: 'Not found' }, 404);
});

app.listen(3000);
// app.js, Express 5
import express from 'express';

const books = new Map();
export const app = express();

app.use(express.json());

app.get('/books{/:id}', (req, res) => {
  const { id } = req.params;
  if (id === undefined) {
    return res.json([...books.values()]);
  }
  const book = books.get(id);
  if (!book) {
    return res.sendStatus(404);
  }
  res.json(book);
});

app.post('/books', (req, res) => {
  const book = { id: String(books.size + 1), ...(req.body ?? {}) };
  books.set(book.id, book);
  res.status(201).json(book);
});

app.delete('/books/:id', (req, res) => {
  books.delete(req.params.id);
  res.sendStatus(204);
});

app.use((req, res) => {
  res.status(404).json({ error: 'Not found' });
});

if (import.meta.url === `file://${process.argv[1]}`) {
  app.listen(3000, (error) => {
    if (error) throw error;
    console.log('Bookshelf listening on http://localhost:3000');
  });
}

Source: derived from Migrating to Express 5 and Routing. The path-less app.use() at the end replaces the catch-all, books{/:id} replaces the optional parameter, req.params.id replaces req.param(), and the export lets a test import the app without opening a port (see Testing).

Codemods

The Express team publishes codemods for the mechanical part of the migration. They run through the Codemod tool and rewrite source files in place, so run them on a clean working tree and review the diff.

The full recipe is a single command; the yarn, pnpm and bun equivalents are yarn dlx, pnpm dlx and bunx.

npx codemod@latest @expressjs/v5-migration-recipe

Each change can also be applied alone:

npx codemod@latest @expressjs/route-del-to-delete

The recipes listed in the official migration guide, checked on 2026-10-29, are these:

Codemod What it rewrites

@expressjs/route-del-to-delete

app.del() to app.delete().

@expressjs/pluralize-method-names

req.acceptsCharset, req.acceptsEncoding and req.acceptsLanguage to the plural names.

@expressjs/explicit-request-params

req.param() to req.params, req.body or req.query.

@expressjs/status-send-order

res.json, res.jsonp and res.send calls that pass a status as an argument to a chained res.status().

@expressjs/redirect-arg-order

res.redirect(url, status) to res.redirect(status, url).

@expressjs/back-redirect-deprecated

res.redirect('back') (and the equivalent res.location) to an explicit Referrer fallback.

@expressjs/camelcase-sendfile

res.sendfile() to res.sendFile().

@expressjs/sendfile-options

The hidden and from options of res.sendFile() to dotfiles and root.

@expressjs/static-dotfiles

The hidden and from options of express.static().

@expressjs/static-mime

express.static.mime to the mime-types package.

The guide lists no recipe for the path syntax, req.query, req.host, req.params, res.clearCookie, res.status, res.vary, router.param() arrays, the app.listen callback or the urlencoded default. Those are manual edits, and the checklist below covers them. The Express project keeps the up-to-date list of recipes at codemod.link/express; re-read it before a migration, because recipes are added and renamed.

Express 4 to 5 upgrade checklist

Do the steps in order and keep the tests green after each one. A migration that mixes the upgrade with feature work is hard to bisect.

flowchart TD A["Green tests on Express 4"] --> B["Upgrade Node.js to 18 or newer"] B --> C["Run the codemods on a clean tree"] C --> D["Install express@5 and the 5.x middleware"] D --> E["Fix paths, req.body, req.query, req.params"] E --> F["Fix the status, redirect and sendFile leftovers"] F --> G["Update tests and error handlers"] G --> H["Deploy behind a flag or to staging"]
  1. Make the test suite pass on the current Express 4 version, and add tests for the routes that use wildcards, optional parameters, req.query filters and static files. Most of the changes fail only where the code runs.

  2. Move to Node.js 18 or newer. The recommended target is the current LTS (24 at the time of writing); see Versions, Support & LTS.

  3. Upgrade to the newest 4.x first (express@4) and fix the deprecation warnings it prints. Most removals were deprecated in 4.x for years.

  4. Commit, then run npx codemod@latest @expressjs/v5-migration-recipe and review the diff.

  5. Install Express 5 and its companion types: npm install express@5 and, for TypeScript, npm install --save-dev @types/express@5. Update the sibling packages that pin Express 4 (for example the Apollo integration moves to @as-integrations/express5, and middleware that lists express as a peer dependency).

  6. Search for the path patterns that crash at start-up: '*', /*, a parameter followed by ?, a parenthesis or bracket in a string route. Rewrite them with /*splat, /{*splat} and brace groups, or with an array of paths.

  7. Check every place that reads req.body (guard against undefined), req.query (the simple parser, no assignment), req.params (arrays for wildcards, null prototype) and req.host.

  8. Search for leftovers the codemods cannot see: res.send( with a number, app.param(fn), router.param([, res.clearCookie( with maxAge, res.status( with a computed value and res.vary().

  9. Review express.static() and res.sendFile() for dotfiles (/.well-known) and for clients that match on the old .js content type.

  10. Register a JSON error handler last, remove try/catch and shims that only existed to forward async errors, and handle the error argument of the app.listen callback.

  11. Update the DEBUG value in runbooks to include router and router:*.

  12. Update the tests, run them with NODE_ENV=production, and deploy to a staging environment before production. Keep the lock file committed so the rollback to Express 4 is a revert.

The Express 3 to 4 leftovers

Several tutorials, and parts of the older books, were written for Express 3 or the early 4.x releases. Their code fails on every current version, and the symptoms are often confusing because the error mentions a missing function. The changes were made in Express 4.0, published in April 2014.

Express 3 code Express 4 and 5

express.createServer()

Removed. Call express() and let app.listen() or http.createServer(app) create the server.

app.configure() blocks

Removed. Use app.set() with ordinary if statements on process.env.NODE_ENV.

app.use(app.router)

Not needed. Routes run in the order they were registered. app.router came back in Express 5 only as a reference.

Bundled Connect middleware reachable as express.bodyParser() and friends

Removed from the core; each is a separate package (table below). Body parsing returned to the core in 4.16.0 as express.json() and express.urlencoded().

app.del(), req.param(), res.sendfile()

Deprecated in 4.x and removed in 5.0.

The bundled middleware and the packages that replaced them:

Express 3 member Package today

express.bodyParser()

express.json() and express.urlencoded(), or the body-parser package

express.cookieParser()

cookie-parser

express.session()

express-session

express.logger()

morgan

express.methodOverride()

method-override

express.compress()

compression

express.errorHandler()

errorhandler

express.favicon()

serve-favicon

express.directory()

serve-index

express.timeout()

connect-timeout

express.responseTime()

response-time

express.csrf()

csurf, now deprecated (see below)

express.multipart()

Removed; use multer

Source: Migrating to Express 4. The maintained modules are described in Official Middleware Modules; the application API leftovers in Extending the Express API.

Deprecated middleware

The Express team announced on 2025-05-16 that several modules are deprecated and no longer receive fixes. Do not add them to a new project and plan a replacement in existing ones.

Module What to use instead

csurf

SameSite cookies with a maintained token library such as csrf-csrf or csrf-sync; see Security Best Practices.

connect-multiparty

multer 2.x; see File Uploads & Downloads.

path-match

Express 5 routing, or path-to-regexp directly.

connect-timeout is still published but is better replaced by Node.js server timeouts and abortable operations. method-override still works and needs a body parser registered before it. Both are covered in Official Middleware Modules.

Where the books are outdated, insecure or wrong

The four books behind this section target Express 4.0 to 4.18, and one of them was published three months before Express 5.0.0, so none can show Express 5. The statements below are described and corrected, never reproduced. Each row says what changed, in which version, and links to the page that treats the topic in depth. In the columns, "Sharma", "Monteiro 2014", "Monteiro 2016" and "Bugl" refer to the four books in the bibliography on the section landing page.

Express API changes

Older practice Current state Changed in Details

All four books present Express 4 (4.0 to 4.18) as the current line.

Express 5.3.0 is the latest release, Express 4 is on the latest-4 tag, and Express 5 needs Node.js 18 or newer.

5.0 (2024-09), latest since 5.1 (2025-03)

Versions, Support & LTS, Introduction & Architecture

npm install -g express to get a command-line tool.

Express has no CLI. The generator is the separate express-generator package, run with npx; it is still an Express 4 scaffold with Jade as the default view engine.

Express 4.0

Getting Started, Project Structure & Configuration

app.use(bodyParser()), or the separate body-parser package in every project.

The combined function no longer exists. express.json() and express.urlencoded() are built in (4.16.0) and re-export body-parser 2.x in Express 5. extended defaults to false, and req.body is undefined without a parser. A bare bodyParser() or extended: true by habit should be replaced by explicit parsers with a limit.

4.0 (removal), 4.16.0 (built in), 5.0 (defaults)

Body Parsing, Middleware Fundamentals, Routers & Modular Apps, Routing, Validation & Input Handling

res.send(status, body), res.json(status, body), res.send(404) and other status-first signatures.

Removed. Chain res.status(code).send(body) or .json(body), and use res.sendStatus(code) for a bare status. res.send(number) sends a JSON body, it no longer sets a status.

Deprecated in 4.x, removed in 5.0

Response, Building REST APIs, Content Negotiation & HTTP Caching, File Uploads & Downloads, TypeScript with Express

app.use('*', ...), app.all('*', ...), /api/* and /:id? catch-alls and optional parameters, and app.options('*', cors()).

Throw at start-up ("Missing parameter name") with path-to-regexp 8. Use /*splat, /{*splat}, brace groups, an array of paths or a path-less app.use(). A SPA fallback is /{*splat} registered after the API.

5.0

Route Paths & Parameters, Routing, Routers & Modular Apps, SPAs & Server-Side Rendering, CORS

Reading a wildcard as the string req.params[0], app.param(':id', fn) with a colon, router.param(fn), req.param('id'), and hasOwnProperty on req.params.

The wildcard is an array under a name you choose, the colon is ignored, the function-only form is removed, req.param() is removed and req.params has a null prototype (Object.hasOwn()).

5.0

Route Paths & Parameters, Request, Routers & Modular Apps, Extending the Express API

app.del(), res.redirect('back'), res.sendfile() and its hidden option, req.acceptsLanguage() and the other singular forms.

Removed. Use app.delete(), an explicit and validated redirect target, res.sendFile() with dotfiles and root, and the plural req.acceptsLanguages() family.

Deprecated in 4.x, removed in 5.0

Routing, Response, Serving Static Files, Content Negotiation & HTTP Caching

Reading nested query-string filters from req.query with no configuration, and assigning to req.query.

Express 5 defaults to the simple parser and req.query is a read-only getter. Opt in to extended deliberately, bound the depth and validate, because nested objects enable operator injection.

5.0

Application & Settings, Request, Validation & Input Handling

Using req.host as a bare host name or building absolute links from the Host header.

req.host includes the port. Use req.hostname, and build links from configuration.

5.0

Request

A try/catch in every async handler and no central error handler, or if (err) res.send(err) without return.

Express 5 forwards rejected promises; a missing return sends two responses (ERR_HTTP_HEADERS_SENT). Register one JSON error handler last, or an express-jwt 401 or a body-parser 400 reaches the client as the default HTML page. Shims that patch Express for async errors are obsolete.

5.0

Error Handling, Middleware Fundamentals, File Uploads & Downloads, SPAs & Server-Side Rendering, Building REST APIs

Ad hoc error bodies such as an error key or a success flag.

Prefer RFC 9457 application/problem+json, so that clients and tools handle errors uniformly.

Practice

Error Handling, Building REST APIs

app.listen() without handling the failure to listen.

In Express 5 the callback receives the error (for example EADDRINUSE); on Express 4 listen for the server error event. Handle SIGTERM and SIGINT and drain connections before exiting.

5.0

Getting Started, Application & Settings, Error Handling, Health Checks & Graceful Shutdown, Project Structure & Configuration

serve-static imported directly without being declared, express.static.mime.define(), a maxAge read as seconds, and assuming /.well-known is served.

Use express.static, which is the same function. express.static.mime is removed (set Content-Type in setHeaders), maxAge is in milliseconds, and dotfiles are ignored by default.

5.0

Serving Static Files, Content Negotiation & HTTP Caching

res.render() assumed to run synchronously, Jade as the default engine, and app.configure(), express.createServer() and app.router taken from Express 2 and 3 tutorials.

res.render() is always asynchronous. app.configure() and express.createServer() were removed in 4.0; app.router returned in 5 as a plain reference and is never mounted by hand.

4.0, 5.0

Response, Template Engines & Views, Application & Settings, Extending the Express API, Routers & Modular Apps

Plugin and app code that builds paths with __dirname, var and CommonJS, and callbacks throughout three books.

Express 5 supports CommonJS and ESM. In ESM use import.meta.dirname (Node.js 20.11 and later) or fileURLToPath(import.meta.url), with async and await.

Practice

Application & Settings, Serving Static Files, Routers & Modular Apps, Introduction & Architecture

Middleware, sessions and authentication

Older practice Current state Changed in Details

express.bodyParser(), express.cookieParser(), express.session(), express.logger(), express.methodOverride(), express.compress() and express.multipart().

Removed from the core; each is a separate package. express.csrf() and the csurf module are gone or deprecated, and connect-multiparty is deprecated (use multer 2.x).

4.0; deprecations announced 2025-05

Official Middleware Modules, Performance Best Practices, Debugging & Logging, Security Best Practices

connect-timeout applied globally, and serve-favicon as the only way to avoid favicon noise.

Both still work, but Node.js server timeouts with abortable operations and express.static() cover most uses.

Practice

Official Middleware Modules

express-session with resave: true, saveUninitialized: true, httpOnly: false, the default memory store, no secure or sameSite, and no trust proxy behind a platform proxy.

Both flags false (omitting them prints a deprecation warning), an HttpOnly, Secure, SameSite cookie, a custom cookie name, a Redis or database store, a rotated secret from the environment, and an exact trust proxy setting. cookie-parser has not been required since express-session 1.5.0, and maxAge is in milliseconds.

Practice; session 1.5.0

Cookies & Sessions, Authentication & Authorization, Security Best Practices, Behind Proxies

require('connect-mongo')(session) and new MongoStore({ mongooseConnection }).

MongoStore.create({ mongoUrl }) or { client }.

connect-mongo 4

Cookies & Sessions, MongoDB & Mongoose

req.logout() without a callback, logout through GET, session fixation ignored, and middleware registered before session().

Passport 0.6 and later needs req.logout(callback) and regenerates the session id at login; logout is a POST with CSRF protection; mount session(), then passport.initialize() and passport.session(), then the routes.

Passport 0.6

Authentication & Authorization, Cookies & Sessions, Middleware Fundamentals

JWTs with no expiry, a custom JWT scheme, tokens in localStorage, no refresh or revocation, and express-jwt with no pinned algorithm.

Short exp, Authorization: Bearer, the token in memory, a rotated refresh token in an HttpOnly cookie with reuse detection, and a deny list. algorithms is mandatory in express-jwt 8 and the payload is on req.auth.

express-jwt 8

Authentication & Authorization

A first OAuth login that passes null as the user, and bcrypt-nodejs used synchronously at a low cost.

Create the local user at first login, identify by issuer and subject, and use authorization code with PKCE. bcrypt-nodejs is unmaintained: use bcrypt (cost 12 or more) or argon2id with the asynchronous API.

Practice

Authentication & Authorization, Security Best Practices

Deploying behind a PaaS router with secure cookies or IP logic and no trust proxy; app.enable('trust proxy') as a blanket fix; reading req.connection.remoteAddress or raw x-forwarded-for.

Cookies marked secure are never set and req.ip is the proxy’s address unless trust proxy names the proxy. Use a hop count or an address list, req.ip and req.ips, and req.socket (req.connection is deprecated in Node.js).

Practice

Behind Proxies, Application & Settings, Request

cors() open to every origin, registered after the routes, or with a wildcard Socket.IO origin.

An explicit allow-list from configuration, cors() registered before the routes and the error handlers, credentials only for named origins. cors 2.8.6 is still maintained.

Practice

CORS, GraphQL with Express, Security Best Practices, Real-Time: WebSockets & SSE

Data, input and output

Older practice Current state Changed in Details

Mongoose callbacks (Model.find(query, cb), save(cb), exec(cb)).

Removed; use promises and async/await, and Express 5 forwards the rejection.

Mongoose 7

MongoDB & Mongoose, Building REST APIs

remove, findOneAndRemove, findByIdAndRemove and Model.count().

Use deleteOne, deleteMany, findOneAndDelete, findByIdAndDelete, countDocuments and estimatedDocumentCount.

Mongoose 8

MongoDB & Mongoose, Building REST APIs

useMongoClient, useNewUrlParser, useUnifiedTopology, mongoose.Promise = global.Promise, and ensureIndex.

useMongoClient ended in Mongoose 5; the other two options are no-ops; native promises are the only option; ensureIndex was removed from the drivers and servers 5.0 and later (use schema indexes and syncIndexes()).

Mongoose 5, MongoDB 5.0

MongoDB & Mongoose

Model.find(req.query), Model.create(req.body), opening a connection per request, and comparing ObjectIds with ===.

Pick fields explicitly, validate first, consider sanitizeFilter, connect once at start-up, and compare with a.equals(b).

Practice

MongoDB & Mongoose, Validation & Input Handling

sequelize.sync() at start-up, operator aliases such as $gt, the callback style, and sequelize.import().

Use migrations (sequelize-cli or Umzug), the Op symbols and promises; sequelize.import() was removed. Connect, check, and only then listen. Clamp limit and prefer keyset pagination.

Sequelize 6

Database Integration

req.assert(), req.validationErrors() and the expressValidator() middleware.

Removed; use body(), validationResult() and matchedData(), or a schema library.

express-validator 6 and 7

Validation & Input Handling

Jade as the generator default, <% include file %> in EJS, Swig, consolidate, and connect-flash.

Jade was renamed Pug in 2016 and the jade package is deprecated; EJS 3 removed the directive (use include()); Swig and consolidate are unmaintained (Nunjucks or @ladjs/consolidate); the flash helpers are small and little maintained.

Pug 2016, EJS 3

Template Engines & Views, Application & Settings

Stack traces or error messages rendered with an unescaped EJS tag, and req.body or req.query passed straight to res.render.

A cross-site scripting hole. Escape the output, hide stacks in production and name the values you pass.

Practice

Template Engines & Views, Error Handling, Security Best Practices

Uploads written under public/ with req.file.originalname, the client MIME type trusted, a misspelt multipart enctype, and multer 0.x or 1.x with no limits.

Generate the stored name, verify the content and re-encode images, store outside public/, and use Multer 2 with limits, fileFilter and a MulterError handler. Image hosting accounts give way to object storage with presigned URLs.

Multer 2

File Uploads & Downloads, Serving Static Files, Request, Security Best Practices

Mass assignment (a role or author taken from req.body), password hashes returned in responses, and a numeric id passed to findById unchecked.

Allow-list the accepted fields, take the owner from the authenticated user, build every response from a public shape, and validate identifiers (answer 400 or 404, not 500).

Practice

Request, Response, Validation & Input Handling, Authentication & Authorization, Building REST APIs

POST answered with 200 or the bare document, unpaginated collections, filters in the path and no API description.

Answer 201 with a Location header, 204 for deletes, paginate with a capped limit, whitelist filters and sort keys, and keep an OpenAPI document.

Practice

Building REST APIs

Sitemaps built from unescaped strings, index.html returned for every GET, and a permissive production CORS with a separate dev server.

Escape values and respect the 50,000-URL limit, skip paths with a file extension, and prefer a dev-server proxy and one origin in production.

Practice

SPAs & Server-Side Rendering

Weak ETags ignored, disabled or mixed up with Last-Modified.

Weak ETags are on by default and combine with Last-Modified; set the etag option deliberately.

Practice

Content Negotiation & HTTP Caching

Ecosystem, hosting and tooling

Older practice Current state Changed in Details

The Apollo Server 4 integration imported from @apollo/server/express4.

Apollo Server 4 reached end of life in January 2026 (check Apollo’s version policy). Apollo Server 5 moved the Express integration to @as-integrations/express5; Express 4 applications use @as-integrations/express4. await apollo.start() is required first, introspection and the explorer need an explicit setting, and demand control is missing from the book.

Apollo Server 5

GraphQL with Express

require('socket.io').listen(server), origin: '*', unauthenticated sockets, identity sent by the client, session sharing through helper packages, and sticky sessions not mentioned.

new Server(httpServer, options); Socket.IO 3 and later block cross-origin requests unless cors lists them; authenticate the handshake with io.engine.use() or a verified token in auth; add an adapter and sticky sessions for several nodes. Server-sent events are often enough for one-way updates.

Socket.IO 3 and 4

Real-Time: WebSockets & SSE

LoopBack 2/3 and slc, KeystoneJS 0.x/4, MEAN.JS, Bower, Grunt, Gulp 3, Yeoman generators, AngularJS and Protractor.

End of life, archived or deprecated: LoopBack 4 is a rewrite, Keystone 5 and 6 replaced the old one, and current stacks use Vite and a maintained front end. Several frameworks still pin Express 4, so the Express 5 changes may not apply to them. Koa, Fastify, Hono and NestJS are mainstream options.

Practice

Frameworks Built on Express & Alternatives, Introduction & Architecture, Project Structure & Configuration

Heroku free dynos, mLab/MongoLab, MongoHQ/Compose, travis-ci.org, Shippable and Codeship.

The Heroku free tier ended in November 2022 (the platform still exists), mLab and MongoHQ are gone (MongoDB Atlas replaced them), travis-ci.org was shut down, Shippable was discontinued and Codeship is legacy. GitHub Actions and similar hosted runners replaced the pipelines.

2022 and later

Deployment & Containers, Health Checks & Graceful Shutdown, Performance Best Practices, Database Integration, Behind Proxies

forever or nodemon to keep a production server alive, and npm install in the deploy script.

Use systemd, a container restart policy or an orchestrator in production; node --watch and node --env-file cover development; deploy with npm ci --omit=dev and exec-form node as PID 1 so SIGTERM reaches the application.

Node.js 18 and later

Deployment & Containers, Performance Best Practices, Health Checks & Graceful Shutdown, Getting Started, Debugging & Logging

node debug, --debug and node-inspector, console.log tracing, morgan as the only logger, Winston and Bunyan, and uncaughtException handlers that keep the process alive.

Use node --inspect, the DEBUG variable and structured logging with pino and AsyncLocalStorage; let the process crash and restart it. closeIdleConnections() and closeAllConnections() are built into Node.js 18.2 and later.

Node.js 18

Debugging & Logging, Performance Best Practices, Health Checks & Graceful Shutdown

Mocha through Grunt, chai-http, Sinon, istanbul and nyc, a real development database, Selenium and PhantomJS, and tests that import a module calling app.listen().

supertest with node --test, Jest or Vitest; node:test mocks; built-in coverage or c8; mongodb-memory-server or Testcontainers; Playwright. Export app (or a factory) and keep listen in a server module.

Practice

Testing, Project Structure & Configuration

Running TypeScript through ts-node for every run, import express = require('express'), @types/express 4 with Express 5, and req.body typed as the model.

Node.js strips types natively from 22.18 and 23.6 (use erasableSyntaxOnly), the official setup is ESM with nodenext, @types/express 5 matches the new path syntax, and req.body is any (and undefined before a parser).

Node.js 22.18, @types/express 5

TypeScript with Express

process.env secrets committed in .env files, loaded only through dotenv.

Ignore .env, commit .env.example, validate at start-up and rotate anything that was committed. Node.js has --env-file and process.loadEnvFile().

Node.js 20.6

Project Structure & Configuration, Getting Started, Security Best Practices

One process, a hand-written cluster, os.cpus().length, synchronous template and file work, and express.compress().

node:cluster, PM2 or one container per core with os.availableParallelism(); worker threads for CPU work; install compression or compress at the proxy; --trace-sync-io finds blocking calls.

Practice

Performance Best Practices

Security problems the books leave in place

Security corrections are spread over the rows above (sessions, JWTs, CORS, mass assignment, uploads, stack traces, secrets). The rows below add the controls that none of the four books covers.

Weakness Current practice Details

Session, JWT, OAuth and database secrets written in source, and .env files committed to companion repositories.

Environment variables or a secret manager, .env.example, secret scanning, start-up validation, and rotation of anything that was ever committed.

Security Best Practices, Project Structure & Configuration, Deployment & Containers

No Helmet, no rate limiting, no input validation and no CSRF protection in any book; csurf is now deprecated.

One middleware sets the standard headers, per-route and per-account rate limits use a shared store, schemas validate every input, and SameSite cookies are paired with csrf-csrf or csrf-sync.

Security Best Practices, Validation & Input Handling, Authentication & Authorization

Inline regular expressions in route strings that relied on path-to-regexp 0.1.x, and year-old lock files with no dependency audit.

path-to-regexp 8 removes the route-level regular expression attack class; use npm audit, npm ci and automated update pull requests.

Security Best Practices, Route Paths & Parameters

X-Powered-By: Express left on.

Disable the x-powered-by setting and add Helmet.

Application & Settings, Security Best Practices

Where each page keeps its own list

Every concept page in the section has a "Where the books are outdated" subsection written for the topic of that page, with the chapter-level attribution of each practice. The consolidated tables above merge them; use the index below to jump to the original.

Page Subsection scope

Introduction & Architecture

The Express 4 era, the generator, body parsing, status signatures, the old front-end stack.

Getting Started

The generator, Jade, listen errors, nodemon and dotenv, secrets, body parsing, tests that start a server.

Versions, Support & LTS

Which Express line is current, global installs, Node.js minimums, hosting and CI of 2014 to 2018.

Application & Settings

bin/www, the view engine, __dirname in ESM, the query parser, trust proxy and x-powered-by.

Project Structure & Configuration

Global installs, the generated layout, the app and server split, secrets, --env-file, blocking calls.

Middleware Fundamentals

bodyParser(), double responses, async handlers, catch-alls, ordering, status-first signatures.

Official Middleware Modules

The Connect middleware removed in Express 4, csurf, connect-multiparty, connect-timeout, method-override.

Extending the Express API

app.configure(), express.createServer(), removed helpers, typings for extensions.

Routing

app.del(), catch-all and optional paths, status-first signatures, bodyParser().

Route Paths & Parameters

Catch-alls, optional parameters, wildcard values, app.param(':id'), the null-prototype req.params.

Routers & Modular Apps

CommonJS routers, app.router, router.param(fn), req.param(), per-handler try/catch.

Request

req.param(), mass assignment, id checks, upload names, the query parser, req.host, proxies.

Response

Status-first calls, res.send(404), back, sendfile, leaked hashes, cookie flags, synchronous render.

Body Parsing

bodyParser(), the separate package, extended, req.body shape, multipart, limits and errors.

Content Negotiation & HTTP Caching

Singular accepts methods, status-first signatures, maxAge units, ETags.

Cookies & Sessions

Session flags, stores, connect-mongo, secrets, fixation, logout, cookie-parser, maxAge units.

Authentication & Authorization

bcrypt-nodejs, Passport 0.6, JWT lifetimes, OAuth, middleware order, committed secrets.

Security Best Practices

Secrets, sessions, Helmet, rate limiting, CSRF, CORS, mass assignment, uploads, stack traces, audits.

CORS

Open cors(), registration order, app.options('*'), wildcard origins, body-parser era tutorials.

Behind Proxies

trust proxy on Heroku, true as a blanket fix, req.connection, retired hosts.

Error Handling

Per-handler try/catch, missing handlers, double responses, leaked errors, async shims, error shapes.

Validation & Input Handling

No validation, express-validator 2, mass assignment, injection, id formats, extended.

Building REST APIs

Status-first calls, body-parser, status codes, mass assignment, error bodies, catch-alls, Mongoose.

Database Integration

sequelize.sync(), operator aliases, sequelize.import(), committed credentials, pagination.

MongoDB & Mongoose

Callbacks, removed methods, connection options, ensureIndex, connect-mongo, mLab, injection, ObjectIds.

Template Engines & Views

Jade and Pug, EJS 3, Swig, consolidate, flash helpers, async render, escaping.

Serving Static Files

Root paths, Bower, serve-static, dotfiles, express.static.mime, uploads, sendfile.

File Uploads & Downloads

Enctype, original names, MIME trust, Multer versions, error handling, storage, res.send(status).

SPAs & Server-Side Rendering

Catch-all fallbacks, missing assets, CORS and dev servers, body-parser, sitemaps.

GraphQL with Express

Apollo Server 4 integration, Express 4, CORS and limits, start order, introspection, demand control.

Real-Time: WebSockets & SSE

Socket.IO listen, origins, handshake authentication, session sharing, sticky sessions, SSE.

Testing

chai-http, Mocha and Grunt, Sinon, fixed ports, databases, PhantomJS, callbacks, coverage tools.

Deployment & Containers

Heroku, mLab, Travis and friends, forever, npm install, PID 1, committed secrets.

Health Checks & Graceful Shutdown

No signal handlers, listen errors, forever, ping endpoints, hand-tracked sockets, Heroku grace period.

Performance Best Practices

Process supervisors, clustering, bundled logger and compress, uncaughtException, workers, stateless design.

Debugging & Logging

console.log, node debug, nodemon, morgan only, Winston and Bunyan, the Connect logger.

TypeScript with Express

ts-node, import =, erasable syntax, @types/express 5, req.body typing, status-first signatures.

Frameworks Built on Express & Alternatives

LoopBack, KeystoneJS, MEAN.JS, framework pins on Express 4, the other Node.js choices.

After migrating

Run the whole test suite, then exercise the application with a client that sends requests the tests do not: a form post with brackets in the field names, a request to /.well-known/, a wildcard route with a deep path, a call with no body, a failed listen, and a rejected promise in a handler. Re-read Versions, Support & LTS for the maintenance plan of the line you chose, and keep the lock file and a scheduled dependency update in place so the next upgrade is small.