Application and Settings

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, and the examples were run against Express 5.3.0 on Node.js 24. The application is the object returned by express(): it holds the settings that tune Express, the list of middleware and routes, the local variables shared by every template, and the method that starts an HTTP server. Almost every other page in this section configures something on it, so this page is the reference for what the application object is, what each setting does, and how the application is turned into a running server. The examples 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 express() returns

express() is the function exported by the express package, and calling it creates an application. The object it returns is itself a plain JavaScript function with the signature (req, res), with the application methods and the settings store attached to it. That is why the same object can be handed to any HTTP server that accepts a request listener, as http.createServer(app) does, and why app.listen() is only a convenience wrapper around that call.

The package exports more than the factory. These are the other members, covered on the page that owns each of them:

Export What it is Page

express.Router()

Creates a mountable mini-application with its own stack of middleware and routes.

Routers and Modular Apps

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

Request body parsers backed by body-parser 2.x.

Body Parsing

express.static()

Static file middleware backed by serve-static 2.x and send 1.x.

Serving Static Files

express.application, express.request, express.response

The prototypes from which every application, request and response inherits. Replacing a property on them changes Express itself.

Extending the Express API

The factory express.createServer() of Express 2 and 3 was removed in Express 4, as was app.configure(). Old tutorials that call either are describing an API that no longer exists; see Where the books are outdated.

An application does five jobs, and the rest of the page takes them in turn:

The settings API

Settings are a small key-value store that lives on app.settings. Express reads some keys itself (the table in Every application setting); you can store your own keys beside them. Five methods work with the store:

Method Behaviour

app.set(name, value)

Stores the value and returns the application, so calls can be chained. For etag, query parser and trust proxy Express also compiles the value into a function and stores it under a second key (etag fn, query parser fn, trust proxy fn); an invalid value for etag throws a TypeError at this point rather than at the first request.

app.get(name)

Returns the stored value. The same method name registers a route when it is called with a path and handlers, so Express tells them apart by argument count: one string argument is a settings read.

app.enable(name), app.disable(name)

Shortcuts for app.set(name, true) and app.set(name, false). Both return the application.

app.enabled(name), app.disabled(name)

Return true when the setting is truthy (enabled) or falsy (disabled). A key that was never set counts as disabled.

app.set(name) with a single argument is a synonym of app.get(name). Because the store is shared, avoid naming a custom key like a built-in one. A common and perfectly valid use of the store is application-wide configuration read from other modules through req.app.get(name), for example a page size limit; for anything larger than a handful of values, a validated configuration module (see Project Structure and Configuration) is easier to test.

The following module builds the Bookshelf application with the settings that every later page assumes. It exports the application without listening, which is the split that lets tests import it (see Starting a server).

// src/app.js
import { join } from 'node:path';
import express from 'express';

export function createApp({ env = process.env.NODE_ENV ?? 'development' } = {}) {
  const app = express();

  // Routing settings first: they are read when the router is created, on the first
  // app.use() or app.METHOD() call, so set them before registering anything.
  app.enable('strict routing');
  app.disable('case sensitive routing');

  // Security and fingerprinting.
  app.disable('x-powered-by');
  app.set('trust proxy', env === 'production' ? 1 : false);

  // Request parsing and JSON output.
  app.set('query parser', 'extended');
  app.set('json spaces', env === 'production' ? 0 : 2);

  // Views: an absolute path, not one that depends on the working directory.
  app.set('views', join(import.meta.dirname, 'views'));
  app.set('view engine', 'html');

  // Custom keys live in the same store.
  app.set('page size limit', 50);

  app.get('/health', (req, res) => {
    res.json({ status: 'ok', env: req.app.get('env'), limit: req.app.get('page size limit') });
  });

  return app;
}

import.meta.dirname exists from Node.js 20.11 and 21.2. On Node.js 18 use fileURLToPath(new URL('./views', import.meta.url)) instead.

Every application setting

The table lists the settings that Express reads, with their default in Express 5.3 and what each one changes. The defaults were confirmed by reading app.settings of a fresh application. The detailed sections after the table cover the ones that need more than a line.

Setting Type Default Effect

case sensitive routing

Boolean

false

When enabled, /Books and /books are different routes. Disabled, they match the same route.

env

String

process.env.NODE_ENV, or development

The environment name, read once when the application is created. Controls the default of view cache and whether the default error handler shows stack traces.

etag

Boolean, String or Function

weak

How the ETag header of res.send() and res.json() responses is generated. See etag.

jsonp callback name

String

callback

The query-string parameter that res.jsonp() reads to find the callback function name.

json escape

Boolean

false

Escapes <, > and & in the output of res.json() and res.jsonp() as Unicode escapes.

json replacer

Function, Array

none

The replacer argument passed to JSON.stringify() by res.json() and res.jsonp().

json spaces

Number or String

none

The space argument passed to JSON.stringify(), which pretty-prints the output.

query parser

Boolean, String or Function

simple

How req.query is parsed. See query parser.

strict routing

Boolean

false

When enabled, /books and /books/ are different routes. Disabled, they match the same route.

subdomain offset

Number

2

How many dot-separated parts at the end of the host name to remove to get req.subdomains.

trust proxy

Boolean, String, Number, Array or Function

false

Which reverse proxies are trusted to report the client address, protocol and host. See trust proxy.

views

String or Array

views directory under the process working directory

The directory, or array of directories, searched for view files.

view cache

Boolean

true when env is production, otherwise off

Caches the resolved view (and, for engines that support it, the compiled template) between renders.

view engine

String

none

The default template engine extension, used when res.render() is called without a file extension.

x-powered-by

Boolean

true

Sends the X-Powered-By: Express response header.

Reading app.settings of a fresh Express 5.3 application also shows some keys that are not in the table: etag fn, query parser fn and trust proxy fn hold the compiled forms described above, and view holds the View class that res.render() instantiates. Assigning a class to view replaces how views are looked up, which is the technique the official view-constructor example uses to render from a source other than the file system (see Template Engines and Views).

env

app.get('env') is the application’s idea of the environment. Express computes it once, in express(), from process.env.NODE_ENV and falls back to development; setting process.env.NODE_ENV afterwards does not change it, but app.set('env', 'production') does. Two behaviours depend on the value in Express itself: when it is production, view cache is switched on, and the default error handler (from finalhandler) stops sending the stack trace to the client. Many third-party libraries read NODE_ENV directly and ignore the Express setting, so set the environment variable and let env follow it. The full effect of NODE_ENV=production is described in Performance Best Practices.

case sensitive routing and strict routing

Both settings are passed to the router when it is created. Express creates the router lazily, on the first call of app.use(), app.METHOD(), app.all() or any access to app.router, so changing either setting after a route has been registered has no effect on the routes that already exist. Set them first, as the example above does, and set them again on every sub-application and express.Router() that needs them: a router created with express.Router() takes its own caseSensitive and strict options and does not read the application’s settings. The matching rules, and what path-to-regexp 8 does with trailing slashes, are in Route Paths and Parameters.

etag

When res.send(), res.json() or res.sendStatus() sends a body, Express generates an ETag header from it unless the response already has one. The setting decides how:

  • true or weak: a weak validator (the value starts with W/), the default;

  • false: no ETag is generated;

  • strong: a strong validator, which promises that the bytes are identical;

  • a function (body, encoding) that receives the body as a Buffer and the encoding, and returns the header value. This is the hook for a cheaper or content-aware hash, as in the example below.

Any other value throws a TypeError (unknown value for etag function) as soon as app.set('etag', value) runs. The setting does not affect res.sendFile() or express.static(), which have their own etag option and take the validator from the file’s size and modification time. How ETags drive conditional requests and 304 responses is the subject of Content Negotiation and HTTP Caching.

The json settings and jsonp callback name

json spaces, json replacer and json escape configure how res.json() and res.jsonp() serialise a value, and therefore every JSON response of the API at once. json spaces pretty-prints; leave it unset in production because it makes every response larger. json replacer is the replacer of JSON.stringify(), a function or an array of allowed keys, and works as a last line of defence that removes a field from every response. Do not treat it as the way to keep secrets out of responses: shape the response explicitly, as described in Validation and Input Handling. json escape replaces <, > and & with the JSON escapes \u003c, \u003e and \u0026, which helps when a JSON response might be sniffed as HTML or when the JSON is embedded in a page. jsonp callback name only matters for res.jsonp(), a legacy cross-origin technique that CORS has replaced; do not build new endpoints on it.

// settings-demo.mjs
import express from 'express';

const app = express();

app.disable('x-powered-by');
app.set('json spaces', 2);
app.enable('json escape');
app.set('json replacer', (key, value) => (key === 'internalNote' ? undefined : value));

// Tag from the body length only, to show the hook. Prefer a real content hash.
app.set('etag', (body, encoding) => `"len-${Buffer.byteLength(body, encoding)}"`);

app.get('/books/1', (req, res) => {
  res.json({ id: 1, title: 'Dune <1965>', internalNote: 'never public' });
});

const server = app.listen(3000, (error) => {
  if (error) throw error;
  console.log('listening on http://localhost:3000');
});

process.on('SIGINT', () => server.close());
GET /books/1 HTTP/1.1
Host: localhost:3000

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
ETag: "len-49"

{
  "id": 1,
  "title": "Dune \u003c1965\u003e"
}

The internalNote field was removed by the replacer, the angle brackets were escaped, and the output is indented by two spaces. The ETag value is computed by the custom function from the serialised body.

query parser

req.query is built from the query string by a parser chosen by this setting:

  • simple (the default in Express 5) uses Node.js node:querystring. A key is a flat string: filter[genre]=scifi produces the key filter[genre]. Repeated keys become an array.

  • extended uses the qs library. Brackets build nested objects and arrays: filter[genre]=scifi produces { filter: { genre: 'scifi' } }. This was the default in Express 4.

  • false disables parsing, and req.query is an empty object. Use it when you read req.url yourself.

  • a function (queryString) => object plugs in your own parser.

// query-parser-demo.mjs
import { once } from 'node:events';
import express from 'express';

for (const mode of ['simple', 'extended']) {
  const app = express();
  app.set('query parser', mode);
  app.get('/books', (req, res) => res.json(req.query));

  const server = app.listen(0);
  await once(server, 'listening');
  const { port } = server.address();
  const response = await fetch(`http://localhost:${port}/books?filter[genre]=scifi&tag=a&tag=b&x[]=1`);
  console.log(mode, await response.text());
  server.close();
}
simple {"filter[genre]":"scifi","tag":["a","b"],"x[]":"1"}
extended {"filter":{"genre":"scifi"},"tag":["a","b"],"x":["1"]}

Two consequences matter in practice. First, a client that sends nested query strings works on Express 4 and silently stops working on Express 5 unless the application sets extended. Second, neither parser gives you a typed value: a number arrives as a string, and a repeated key is an array while a single occurrence is a string, so a handler that expects either must normalise. Validate and coerce req.query with a schema (see Validation and Input Handling). In Express 5 req.query is a getter that parses on access, so assigning to it no longer works; copy it into a new object instead.

trust proxy

Behind a load balancer or reverse proxy, the TCP peer of Express is the proxy, not the client. With trust proxy left at false, req.ip is the proxy’s address, req.protocol is http even when the browser used HTTPS, and a cookie marked secure is never sent. Enabling the setting makes Express read the X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host headers, but only from the proxies you name, because any client can forge those headers when the connection does not come through a proxy you control. The accepted values are:

  • a Boolean: true trusts every hop and takes the left-most address (never do this on a server that can be reached directly); false trusts none;

  • a number: the count of proxy hops in front of the application, for example 1 behind a single load balancer;

  • a string, comma-separated list or array of IP addresses and subnets, which may include the keywords loopback, linklocal and uniquelocal;

  • a function (address, hopIndex) => boolean for custom logic.

A sub-application with no explicit trust proxy of its own inherits the parent’s value. The full explanation, with the values for common platforms and the effect on req.ip, req.ips, req.protocol, req.hostname and req.secure, is in Behind Proxies; the cookie consequences are in Cookies and Sessions.

subdomain offset

req.subdomains is the host name’s labels in reverse order, after removing the last subdomain offset labels. With the default of 2, the host tobi.ferrets.example.com yields ['ferrets', 'tobi']. Set it to 3 for a host such as tobi.ferrets.example.co.uk, because the registrable domain has three labels. The setting only changes how the host header is split; it does not validate the host name, which is client-controlled.

views, view cache and view engine

views is where res.render() and app.render() look for templates. The default is a views directory under the process working directory, not under the file that creates the application, so a program started from another folder will not find its templates; always pass an absolute path, as in the first example. An array is searched in order, which lets a theme override a default template. view engine sets the extension used when the name has none, so with app.set('view engine', 'pug') the call res.render('index') loads index.pug. The engine package is loaded on first use with require(), which is why the engine must be installed and (for ESM-only engines) why you may need app.engine(), described in app.engine(), app.render() and app.path(). view cache makes Express remember the result of the file lookup and pass cache: true to engines that cache compiled templates; it is on in production and should stay off while developing so edits appear on reload. The engines themselves, layouts and escaping are covered in Template Engines and Views.

x-powered-by

By default every response carries X-Powered-By: Express, which tells scanners which framework (and so which vulnerabilities) to try. Turn it off with app.disable('x-powered-by'), or use Helmet, which removes the header as part of its defaults. It is a hygiene measure, not a defence: the framework is usually identifiable from other signals. See Security Best Practices.

app.locals

app.locals is a plain object whose properties become variables in every template rendered by the application. Unlike res.locals, which lives for a single request, app.locals lasts for the application’s lifetime, so it is the right place for values that never change per request: the site title, an asset version, a formatting helper. It also carries one property set by Express, app.locals.settings, which is the same object as app.settings, so templates can read settings such as settings.env.

When res.render() or app.render() builds the template’s variables, the three sources are merged in this order, with later ones winning: app.locals, then res.locals, then the object passed to the render call. A value passed to the call always overrides the other two, and a res.locals value set by middleware overrides a global default. Do not put per-request data on app.locals: the object is shared between concurrent requests, so one user’s data would leak into another user’s page. Note also that app.locals is a template concept; for configuration read in handlers use settings or a configuration module.

The following program defines a template engine with app.engine(), sets locals at both levels, renders through a route and through app.render(), and shows the precedence.

// render-demo.mjs
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import express from 'express';

await mkdir(join(import.meta.dirname, 'views'), { recursive: true });
await writeFile(join(import.meta.dirname, 'views', 'hello.bks'), '<h1>{{title}}</h1><p>{{who}}</p>');

const escapeHtml = (text) => String(text).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);

const app = express();

// An engine is a function (filePath, options, callback). `options` holds every merged local
// plus Express's own `settings` and `cache` entries.
app.engine('bks', (filePath, options, callback) => {
  readFile(filePath, 'utf8').then(
    (template) => {
      const html = template.replace(/\{\{(\w+)\}\}/g, (_, key) => escapeHtml(options[key] ?? ''));
      callback(null, html);
    },
    callback,
  );
});
app.set('views', join(import.meta.dirname, 'views'));
app.set('view engine', 'bks');

app.locals.title = 'Bookshelf';
app.locals.who = 'from app.locals';

app.get('/hello', (req, res) => {
  res.locals.who = 'from res.locals';        // overrides app.locals for this request
  res.render('hello', { title: 'Render call' }); // overrides both
});

const server = app.listen(0, async () => {
  const { port } = server.address();
  console.log(await (await fetch(`http://localhost:${port}/hello`)).text());
  app.render('hello', {}, (error, html) => {
    console.log(error, html);
    server.close();
  });
});
<h1>Render call</h1><p>from res.locals</p>
null <h1>Bookshelf</h1><p>from app.locals</p>

app.engine(), app.render() and app.path()

app.engine(ext, callback) registers a template engine for a file extension. The callback has the signature (filePath, options, callback), as in the example above, and calls callback(error, html) when done. Engines that export a function named __express (Pug, EJS) are picked up automatically through view engine; use app.engine() for the others, for ESM-only engines and to map an extension to an engine, such as app.engine('html', engine.renderFile). Writing an engine is described in the official page for developing template engines, and the markdown example of the expressjs/express repository is a complete engine in a few lines.

app.render(view, [locals], callback) does what res.render() does but returns the HTML to a callback instead of sending it. It is the tool for rendering an e-mail body, a static page at build time or a fragment for a test. res.render() calls it and passes the result to res.send(). In Express 5 the callback is always invoked asynchronously, even for engines that render synchronously; Express 4 let a synchronous engine call back before render() returned, which could expose ordering bugs. A render error goes to the callback, or to next(err) when res.render() is called without one.

app.path() returns the canonical path of the application: an empty string for the top-level application, and the mount path for a sub-application. If the mount path was an array, the returned string is the array joined with commas, as shown in the example of the next section, so do not rely on it in that case.

Source for this section: Express: Developing template engines

Sub-applications, app.mountpath and the mount event

An application can be mounted inside another one with app.use(path, subApp). This is different from mounting an express.Router(): a sub-application is a complete application with its own settings, app.locals, engine registry and error handlers, which makes it the right tool for a self-contained piece such as an administration area with its own view engine. A router is lighter and shares the parent’s settings, so prefer a router unless you need that isolation (see Routers and Modular Apps).

Member Behaviour

app.mountpath

The path pattern, or array of patterns, on which the sub-application was mounted. For the top-level application it is the string /.

app.parent

The parent application once mounted; undefined before.

mount event

Emitted on the sub-application, with the parent application as the only argument, when the sub-application is mounted by app.use(). It is not emitted for routers.

app.path()

The canonical mount path, as described above.

Settings inheritance

A sub-application inherits settings that have no default (for example a custom key). It does not inherit a setting that has a default (query parser, etag, views, x-powered-by, view cache, case sensitive routing and the others in the table): set those on the sub-application. trust proxy is the exception, inherited from the parent unless the sub-application sets its own.

// sub-app.mjs
import express from 'express';

const app = express();
const admin = express();

admin.set('view engine', 'html');              // has a default of "none": set it here
admin.disable('x-powered-by');                 // a default exists: not inherited, set it here

admin.on('mount', (parent) => {
  console.log('admin mounted on', admin.mountpath, 'parent is the main app:', parent === app);
  // Settings without a default are inherited, so a value set on the parent is visible here.
  console.log('page size limit =', admin.get('page size limit'));
});

admin.get('/', (req, res) => {
  res.json({ mountpath: req.app.mountpath, baseUrl: req.baseUrl, path: req.path });
});

app.set('page size limit', 50);
app.use(['/admin', '/manage'], admin);        // an array of mount paths is allowed

console.log('app.mountpath =', app.mountpath, ' admin.path() =', JSON.stringify(admin.path()));

app.listen(3000);
admin mounted on [ '/admin', '/manage' ] parent is the main app: true
page size limit = 50
app.mountpath = /  admin.path() = "/admin,/manage"

Inside a handler of the sub-application, req.app is the sub-application and req.baseUrl is the matched mount path, so a handler should build links from req.baseUrl rather than hard-coding /admin. req.originalUrl always keeps the full original URL. When the mount path is an array, app.mountpath is the array; code that needs a single prefix should take req.baseUrl.

Starting a server

app.listen()

app.listen() creates an http.Server with the application as its request listener and calls server.listen() with the same arguments, so every form of server.listen() works: a port (app.listen(3000)), a port with a host and a backlog, a Unix socket path and a listening callback. It returns the http.Server. Keep that reference: you need it to close the server during a graceful shutdown (see Health Checks and Graceful Shutdown) and to read the actual port when you ask for port 0, which lets the operating system choose a free port and is what tests should use.

Express 5 changed the callback. When the last argument is a function, Express registers it for the listening event and also for the server’s error event, wrapped so that it runs once. On success it is called with no arguments; if the server fails to start (for example EADDRINUSE because the port is taken, or EACCES for a privileged port), it is called with the Error. Express 4 never passed an error, and without a listener for error the process crashed with an unhandled event.

// src/server.js
import { createApp } from './app.js';

const app = createApp();
const port = Number(process.env.PORT ?? 3000);

const server = app.listen(port, (error) => {
  // Express 5: runs once, with an error if the server could not start.
  if (error) {
    console.error(`cannot listen on ${port}: ${error.code ?? error.message}`);
    process.exit(1);
  }
  console.log(`bookshelf-api listening on http://localhost:${server.address().port}`);
});

process.on('SIGTERM', () => {
  server.close(() => process.exit(0));
});

The program starts the application built in the first example. Keeping createApp() free of listen() calls, and putting the listening in a separate entry file, is what allows tests to import the application and call it through supertest without opening a port (see Testing). A callback attached to the listen call of a server that fails after it was listening (an error emitted later) is not called again, because it was registered once; for long-lived errors attach server.on('error', handler) too.

http.createServer(app), HTTPS and HTTP/2

Because the application is a request listener, app.listen() can always be replaced by creating the server yourself. You need to do that to serve HTTPS, to share the server with a WebSocket library (see Real Time: WebSockets and SSE), to listen on several ports with one application, and to set the server timeouts.

// src/server-https.js
import { readFileSync } from 'node:fs';
import http from 'node:http';
import https from 'node:https';
import { createApp } from './app.js';

const app = createApp();

// Placeholders: the paths come from the environment, the key and certificate are never committed.
const tlsOptions = {
  key: readFileSync(process.env.TLS_KEY_FILE),
  cert: readFileSync(process.env.TLS_CERT_FILE),
};

const httpServer = http.createServer(app).listen(8080);
const httpsServer = https.createServer(tlsOptions, app).listen(8443);

// Node.js timeouts worth setting explicitly behind a load balancer.
httpsServer.keepAliveTimeout = 65_000;   // longer than the balancer's idle timeout
httpsServer.headersTimeout = 66_000;     // must exceed keepAliveTimeout

export { httpServer, httpsServer };

Most production deployments terminate TLS at a reverse proxy or load balancer and run Express over plain HTTP, with trust proxy set; that is simpler to operate and renews certificates without restarting the process.

HTTP/2 deserves a caution. Node.js has a compatibility layer in node:http2, but Express was written against the node:http HTTP/1 request and response classes and the official documentation does not describe HTTP/2 as a supported setup, so passing an Express application to http2.createSecureServer() is not a documented configuration. The usual and supported way to offer HTTP/2 to browsers is to terminate it at a proxy or CDN that speaks HTTP/1.1 to Express. Third-party adapters exist; evaluate them for Express 5 compatibility and maintenance before relying on them (see Where the books are outdated for the equivalent warning about unmaintained middleware).

app.router

app.router is the application’s base router: the object that holds the stack built by app.use(), app.METHOD() and app.all(). Express 3 required app.use(app.router) to control the position of the routes in the stack; Express 4 removed the property and made access throw an error with a migration message; Express 5 brings it back as a plain reference to the router, so no app.use(app.router) call is needed or wanted. In other words, app.use(fn) and app.router.use(fn) do the same thing, and the router is created on first access, which is the reason the routing settings must come first (see case sensitive routing and strict routing).

You rarely need the property. Its practical uses are attaching a handler to the base router explicitly and, for debugging, listing the registered routes. The stack array that the example walks is an implementation detail of the router package, not a documented API, so use it for diagnostics only and not as a contract.

// list-routes.mjs
import express from 'express';

const app = express();
app.get('/books', (req, res) => res.json([]));
app.post('/books', express.json(), (req, res) => res.status(201).json(req.body));
app.use('/authors', express.Router().get('/', (req, res) => res.json([])));

for (const layer of app.router.stack) {
  if (layer.route) {
    console.log(Object.keys(layer.route.methods).join(',').toUpperCase(), layer.route.path);
  } else {
    console.log('USE', layer.name);
  }
}
GET /books
POST /books
USE router

Express 4 compared with Express 5

Area Express 4 Express 5

query parser default

extended (qs)

simple (node:querystring); set extended explicitly for nested query strings.

app.listen() callback

Called on listening, never with an error.

Called once, with the Error if the server fails to start.

app.router

Removed; access threw an error.

Back, as a reference to the base router.

app.del()

Deprecated alias of app.delete().

Removed.

app.param(fn) (custom parameter-callback signature)

Deprecated.

Removed. See Route Paths and Parameters.

app.render() callback

Synchronous for synchronous engines.

Always asynchronous.

Path strings in app.use() and app.METHOD()

Parsed by path-to-regexp 0.1.

Parsed by path-to-regexp 8, so '*' and :param? throw. See Route Paths and Parameters.

app.query()

Not available.

Added in 5.3 for the HTTP QUERY method. It needs a Node.js line with QUERY support; see Routing.

app.configure(), express.createServer()

Removed (since Express 4).

Not available.

What changed recently: Express 5.0.0 (2024-09) introduced the changes in the table; 5.1.0 (2025-03-31) became the latest tag on npm, so npm install express installs Express 5 and old projects that depended on the unpinned express name began to receive it on a fresh install; 5.3.0 (2026-10-09) added app.query(). The step-by-step upgrade, with the codemods, is in Migrating and What Changed.

Where the books are outdated

The four consulted books build applications on Express 4.0 to 4.18, and their application-setup code follows the express-generator scaffold of the time. The concepts hold; the details below have changed.

Older practice Where it appears Current behaviour

Creating the server in a bin/www file with http.createServer(app) and storing the port with app.set('port', port).

Generator-based chapters: Sharma, chapter 2; Monteiro 2014, chapters 3 and 4; Monteiro 2016, chapters 1 and 2

Still valid. app.listen() is equivalent, and the app/server split is better kept in two files, as in Starting a server. A custom port setting is only a convention: nothing in Express reads it.

Setting the view engine to jade, with the jade package installed.

The same generator-based chapters

Jade was renamed Pug in 2016 and the old package is deprecated. Use pug, ejs or another maintained engine.

Building the views and static paths with path.join(__dirname, 'views') in CommonJS.

The same generator-based chapters

In ESM there is no __dirname: use import.meta.dirname (Node.js 20.11 and later) or fileURLToPath(import.meta.url). Express 5 supports CommonJS and ESM.

Reading nested query-string parameters from req.query without any configuration.

Books written for Express 4

Express 4 parsed them with qs. Express 5 defaults to the simple parser, so set app.set('query parser', 'extended') or move filters to flat keys.

Calling app.listen() with no error handling.

Bugl

In Express 5 the callback receives the error, which is the place to log and exit instead of crashing on an unhandled error event.

Deploying behind a PaaS router with secure session cookies or IP-based logic and no trust proxy.

Sharma, chapter 10; Monteiro 2014, chapter 8

Cookies marked secure are never set, and req.ip is the proxy’s address, unless trust proxy names the proxy. See trust proxy.

X-Powered-By: Express left on, and no security headers.

All four books

Disable x-powered-by and add Helmet; see x-powered-by.

app.use(app.router) and app.configure() blocks taken from Express 2 and 3 tutorials.

Older tutorials in general

app.configure() was removed in Express 4. app.router returned in Express 5 as a reference, and no app.use(app.router) call is needed.