Getting Started with Express

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 24 LTS, with the Express 4 line (4.22.3) noted wherever it differs. Express 5 declares engines.node as >= 18; Express 4 runs on Node.js 0.10 and later. All facts were checked against the official documentation on 2026-10-29. It takes you from an empty directory to a running, watched, configured Express application, explains the Express 5 behaviour of app.listen(), answers the questions of the official FAQ, and then looks at express-generator, the scaffolding tool that is still shipped as an Express 4 application, and how to bring its output up to date. The application grows into the Bookshelf (a small catalogue of books, authors and reviews) over the following pages. For what Express is and how a request travels through it, see Introduction & Architecture; for choosing between the 4 and 5 lines, see Versions, Support & LTS.

For installing Node.js itself, see Installing Node.js and Release Lines & LTS in the Node.js section.

Requirements

  • Node.js 18 or later for Express 5. The official Installing page states this floor. Use the active LTS release (Node.js 24 at the time of writing) unless you are tied to an older line: the features this page relies on in development (--watch, --env-file, type stripping) are only available from the versions listed in the table below.

  • npm (shipped with Node.js), or any compatible package manager such as pnpm or Yarn. The commands below use npm.

  • A terminal and a text editor. No global installation of Express is needed or recommended.

Feature used on this page Minimum Node.js Notes

Run Express 5

18

Declared in the engines field of the package.

node --watch

18.11 (stable since 20.13 and 22)

Restarts the process when an imported file changes.

node --env-file=.env

20.6

Loads variables from a file into process.env before the script starts.

Running .ts files directly (type stripping)

22.18, or 23.6 on the 23 line

Types are erased, not checked; the official page also requires TypeScript 5.8 or later for type-checking.

app.query() (HTTP QUERY method)

a line with HTTP QUERY support

Covered on the Routing page.

Installing Express

Express is a normal npm package. The official procedure is to create a directory, initialise a package.json, and install the package as a dependency.

mkdir bookshelf-api
cd bookshelf-api
npm init -y
npm install express@5

npm init without -y asks questions, among them the entry point (index.js by default; the official page suggests app.js). npm install express installs the version of the latest dist-tag, which is 5.3.0 today; the explicit @5 range keeps a script from jumping to a future major version. Express 4 stays available on its own dist-tag:

npm view express dist-tags
# { latest: '5.3.0', 'latest-4': '4.22.3' }
npm install express@4        # only if you must stay on the 4 line
npm install express --no-save  # install temporarily, without recording a dependency

Choosing a module system

Node.js decides how to read a .js file from the nearest package.json. To write import statements in files with a .js extension, set the type field of the package.json to module. Without it, .js files are CommonJS and a file ending in .mjs is the way to opt in to ESM for a single file. Express 5 works with both systems, so you choose by style and tooling, not by compatibility.

{
  "name": "bookshelf-api",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "engines": { "node": ">=18" },
  "dependencies": {
    "express": "^5.3.0"
  }
}

The rest of this section uses ESM (the type field set to module, node: prefixes for built-in modules). CommonJS appears only on this page and on pages that are about it.

Hello world

The smallest Express application creates the app, registers a route and starts listening. The official Hello world page gives the same sample in three flavours: index.cjs (CommonJS), index.mjs (ESM) and index.ts (TypeScript).

ESM

// app.js (with "type": "module" in package.json)
import express from 'express';

const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

Run it and request the only route:

node app.js
curl -i http://localhost:3000/
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"

Hello World!

Any other path returns a 404 with the default HTML body produced by finalhandler, the last handler Express calls. The X-Powered-By header is on by default and is usually disabled in real applications; the Security Best Practices page explains why.

CommonJS

// index.cjs (or app.js in a project without "type": "module")
const express = require('express');

const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

The two flavours differ only in the first line. import express from 'express' and require('express') both give you the same express function, which is also the application factory and the holder of the built-in middleware (express.json(), express.static(), express.Router() and others).

TypeScript

Express does not bundle type definitions. Install TypeScript and the community types as development dependencies:

npm install --save-dev typescript @types/express @types/node
// src/app.ts
import express, { type Express, type Request, type Response } from 'express';

const app: Express = express();
const port = 3000;

app.get('/', (req: Request, res: Response) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

The official Installing page recommends this tsconfig.json, which lets Node.js run the file directly and leaves type-checking to the compiler:

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "rewriteRelativeImportExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "strict": true,
    "skipLibCheck": true
  }
}
node src/app.ts      # Node.js strips the types: no build step (22.18+ or 23.6+)
npx tsc              # type-check only: Node.js does not check types

Type stripping erases annotations but cannot transform syntax that generates code, which is why the configuration enables erasableSyntaxOnly (no enum, namespaces or parameter properties) and verbatimModuleSyntax (type imports must be marked with type). Typing requests, responses and handlers is covered on TypeScript with Express.

A first Bookshelf endpoint

The remaining examples build on a version of the hello world that already looks like the start of the Bookshelf API. It reads the port from the environment and answers with JSON.

// app.js
import express from 'express';

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

const books = [
  { id: 1, title: 'Dune', author: 'Frank Herbert' },
  { id: 2, title: 'Neuromancer', author: 'William Gibson' },
];

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

app.listen(port, (error) => {
  if (error) {
    throw error;
  }
  console.log(`Bookshelf API listening on http://localhost:${port}`);
});

app.listen() and its error callback

app.listen() is a convenience method. As the API reference shows, it is equivalent to creating an http.Server with the application as its request listener and calling listen() on it. The application is a plain function with the (req, res) shape, which is why it can also be handed straight to http.createServer() or https.createServer().

// What app.listen() does, written out
import http from 'node:http';
import express from 'express';

const app = express();
const server = http.createServer(app);
server.listen(3000);

The signature is app.listen([port], [host], [backlog], [path], [callback]), and every form of Node.js server.listen() is accepted:

Argument Meaning

port

The TCP port. Omitted or 0 makes the operating system choose a free port (read it with server.address()).

host

The address to bind. Omit it to listen on all interfaces; use 127.0.0.1 for a local-only server.

backlog

The maximum length of the queue of pending connections.

path

A UNIX socket path, as an alternative to host and port.

callback

Called once the server is listening. In Express 5 it also receives the error if the server emits an error event.

The Express 5 error callback

The most common start-up failure is EADDRINUSE: another process already owns the port. In Express 4 the callback of app.listen() was only a listening listener, so the failure surfaced as an unhandled error event that crashed the process with a stack trace unless you attached your own error listener. In Express 5 the callback is also called with the error, so a single function can handle both outcomes. The official reference shows the pattern: throw the error from the callback (or exit with a message), otherwise report success.

import express from 'express';

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

const server = app.listen(port, '0.0.0.0', (error) => {
  if (error) {
    console.error(`Cannot listen on port ${port}: ${error.code}`);
    process.exit(1);
  }
  console.log(`Listening on ${JSON.stringify(server.address())}`);
});

Things to keep in mind:

  • The callback is invoked without arguments on success, so the if (error) check is the way to tell the cases apart.

  • app.listen() returns the http.Server. Keep it: tests, graceful shutdown and WebSocket libraries need it.

  • Code that must run on both lines can attach an error listener to the returned server instead, which works in Express 4 and 5 (server.on('error', handler)).

  • Only start-up errors reach the callback. Runtime errors in handlers are handled by error-handling middleware; see Error Handling.

  • Keep listen() out of the module that builds the app, so tests can import it without opening a port. That split is the first topic of Project Structure & Configuration.

Running with node --watch and --env-file

Node.js now covers the two jobs that nodemon and dotenv used to do in development. Both are command-line flags of the node binary, so nothing needs to be installed.

  • --watch restarts the process when the entry file or any module it imports changes. --watch-path=./src watches a specific directory instead.

  • --env-file=.env reads KEY=value lines from a file into process.env before your code runs. It can be repeated; later files override earlier ones. Node.js 22.9 and later also offers --env-file-if-exists, which ignores a missing file.

# generate a secret instead of inventing one
node -p "require('node:crypto').randomBytes(32).toString('hex')"
# .env  (kept out of Git; commit only .env.example)
PORT=3000
NODE_ENV=development
SESSION_SECRET=replace-with-the-generated-value
{
  "scripts": {
    "start": "node app.js",
    "dev": "node --watch --env-file=.env app.js"
  }
}
npm run dev

A few pitfalls:

  • --watch is a development tool. In production run node app.js under a process manager, a container orchestrator or systemd, and let that supervisor restart it.

  • --env-file does not interpret quotes and expansions like a shell. A variable already present in the real environment takes precedence over the file, which is what you want in production.

  • Never commit .env. The configuration chapter of Project Structure & Configuration shows a validated config module that fails fast when a required variable is missing.

  • Set NODE_ENV=production in production: Express then caches view templates and the generated CSS, and enables other optimisations described on the Performance Best Practices page.

The official FAQ

The Express website keeps a short FAQ. Its answers are spread over the pages of this section; the table gives the official answer in short and the page that develops it.

Question Short answer Developed on

How should I structure my application?

There is no single right answer: Express makes no assumptions about structure. The repository ships the route-separation, route-map and mvc examples.

Project Structure & Configuration

How do I define models?

Express has no model concept or database layer. Use any Node.js database module.

Database Integration

How can I authenticate users?

Express prescribes no scheme; the auth example in the repository shows a simple login and password.

Authentication & Authorization

Which template engines does Express support?

Any engine that follows the (path, locals, callback) signature; consolidate.js normalises the interface and adds caching.

Template Engines & Views

How do I handle 404 responses?

A 404 is not an error, so error middleware never sees it. Add a catch-all middleware at the end of the stack that sets the status.

Error Handling

How do I set up an error handler?

Define middleware with four parameters: (err, req, res, next).

Error Handling

How do I render plain HTML?

You do not need res.render(). Use res.sendFile() for one file or express.static() for a directory.

Serving Static Files

What version of Node.js does Express require?

Express 4 requires Node.js 0.10 or later; Express 5 requires Node.js 18 or later.

Requirements above

Source: Express: FAQ

The two answers that every new application needs are the 404 handler and the error handler, in that order, after all routes:

// after every route
app.use((req, res) => {
  res.status(404).send("Sorry can't find that!");
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

Source: Express: FAQ

express-generator

express-generator is the official application generator. It writes a ready-made directory with an app.js, a start script, a route folder and a view engine. It is the tool behind the scaffolds of many tutorials and of three of the reference books, so you will meet its output in existing code even if you never run it yourself.

Running it

Node.js 8.2 and later can run the generator without a global installation:

npx express-generator --view=pug --git bookshelf-admin
cd bookshelf-admin
npm install
DEBUG=bookshelf-admin:* npm start

The usage is express [options] [dir]. On Windows Command Prompt use set DEBUG=bookshelf-admin:* & npm start, and in PowerShell $env:DEBUG='bookshelf-admin:*'; npm start. Open http://localhost:3000/ to see the welcome page. The DEBUG variable switches on the debug package’s output for the namespaces that match the pattern; see Debugging & Logging.

Options

Option Effect

-h, --help

Print the usage.

--version

Print the generator version.

-e, --ejs

Add EJS support.

--hbs

Add Handlebars support.

--pug

Add Pug support.

-H, --hogan

Add Hogan.js support.

--no-view

Generate without a view engine (an API-only skeleton).

-v, --view <engine>

Choose ejs, hbs, hjs, jade, pug, twig or vash. The default is jade.

-c, --css <engine>

Choose a stylesheet engine: less, stylus, compass or sass. The default is plain CSS.

--git

Add a .gitignore.

-f, --force

Generate into a directory that is not empty.

What it scaffolds

For express --view=pug myapp the official page shows this tree:

.
├── app.js
├── bin
│   └── www
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.pug
    ├── index.pug
    └── layout.pug

The responsibilities are split like this:

  • bin/www creates the HTTP server, picks the port (PORT or 3000), registers the error and listening listeners and starts listening. It is the file npm start runs. This is the app/server split that tests need, done with a script instead of a module.

  • app.js builds and exports the Express application: view engine settings, the logger, the body parsers, the static folder, the routers and the 404 and error handlers.

  • routes/ holds one router module per area (index.js and users.js are placeholders).

  • views/ and public/ hold templates and static assets.

Why the output is an Express 4 application

The package on npm is express-generator 4.16.1, last modified in May 2024. It was not updated for Express 5, so a freshly generated project still has these properties:

  • It depends on the Express 4 line (the package.json pins roughly the 4.16 series), morgan, cookie-parser, debug and http-errors, with pins from 2018 that predate many later security fixes.

  • The default view engine is Jade, the former name of Pug. Jade is deprecated; always pass --view=pug or another engine, or --no-view for an API.

  • The code style is that of 2018: var, CommonJS, function expressions, a hand-written normalizePort and an onError function that handles EACCES and EADDRINUSE in bin/www.

  • The package name is express-generator; since Express 4, npm install -g express no longer installs a command, because the express binary moved out of the express package.

None of this makes the generated app wrong, only old. Treat it as a starting layout and modernise it.

Modernising the generated project

The following steps turn the scaffold into a current Express 5 application. They are the sequence to follow after running the generator, and none of them is generator specific beyond the first.

  1. Upgrade Express and remove the pins of the other packages:

    npm install express@5 morgan@latest cookie-parser@latest debug@latest http-errors@latest pug@latest
  2. Switch to ESM: add the type field set to module to package.json, rename nothing, and convert require to import and module.exports to export default. Add .js extensions to relative imports and replace __dirname with import.meta.dirname (Node.js 20.11 and later).

  3. Replace var with const or let and callbacks with async functions. Express 5 forwards a rejected promise of a handler to the error handlers, so the try/catch and next(err) wrapper is no longer needed.

  4. Delete bin/www and move its job to a small server.js that imports the app and uses the app.listen() error callback from above.

  5. Review every route path and replace /* style catch-alls: Express 5 uses path-to-regexp 8, where a bare * is invalid. See Migrating & What Changed.

  6. Set extended: true on express.urlencoded() only if you need nested form fields; the default is now false.

  7. Move secrets and ports to the environment and run with node --watch --env-file=.env in development.

// app.js: the modernised app, exported without listening
import path from 'node:path';
import express from 'express';
import createError from 'http-errors';
import logger from 'morgan';
import cookieParser from 'cookie-parser';
import indexRouter from './routes/index.js';

const app = express();

app.set('views', path.join(import.meta.dirname, 'views'));
app.set('view engine', 'pug');
app.disable('x-powered-by');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(import.meta.dirname, 'public')));

app.use('/', indexRouter);

app.use((req, res, next) => {
  next(createError(404));
});

app.use((err, req, res, next) => {
  res.status(err.status ?? 500);
  res.render('error', {
    message: err.message,
    error: req.app.get('env') === 'development' ? err : {},
  });
});

export default app;
// server.js: the only file that listens
import app from './app.js';

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

const server = app.listen(port, (error) => {
  if (error) {
    console.error(`Cannot listen on port ${port}: ${error.code}`);
    process.exit(1);
  }
  console.log(`Listening on ${JSON.stringify(server.address())}`);
});
{
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch --env-file=.env server.js"
  }
}

If you want a smaller start, the generator is optional. The FAQ above states that Express makes no assumptions about structure; the generated layout is one of many ways to organise an application.

Generators and boilerplates

Yeoman’s generator-express, MEAN.JS and other boilerplates mentioned in older tutorials follow the same pattern and are mostly unmaintained. The Project Structure & Configuration page explains why such tools are historical and what to put in their place.

Trying the official examples

The expressjs/express repository contains an examples directory with runnable applications. They are the quickest way to see a feature in context, and each one is explained on the page named in the last column.

git clone https://github.com/expressjs/express.git
cd express
npm install
node examples/hello-world

Open the URL the example logs, normally http://localhost:3000. Some examples need extra services (online needs a running Redis server). The repository tracks the current development branch, so for a released version check out its tag before running an example.

Example What it shows Page

hello-world

A simple request handler

Hello world above

route-separation, route-map, mvc

Organising routes per resource, with a map, and with MVC-style controllers

Project Structure & Configuration

resource, params, route-middleware

Multiple operations on one resource, route parameters, route-level middleware

Routing

multi-router, vhost

Several routers, virtual hosts

Routers & Modular Apps

error, error-pages

Error middleware, custom error pages

Error Handling

static-files, downloads

Serving static files, sending files to the client

Serving Static Files

ejs, markdown, view-constructor, view-locals

Template engines and views, rendering dynamically, data kept on the request across middleware

Template Engines & Views

cookies, cookie-sessions, session, online

Cookies, cookie sessions, server sessions, activity tracked in Redis

Cookies & Sessions

auth

Authentication with login and password

Authentication & Authorization

content-negotiation, web-service, search

Content negotiation, a simple API service, a search API

Building REST APIs

The official page also lists one third-party sample, prisma-rest-api-ts in the Prisma examples repository, an Express API in TypeScript with the Prisma ORM; it is not maintained by the Express team.

Where the books are outdated

Older tutorials and the reference books for this section were written for Express 4 (4.0 to 4.18); Express 5 appeared after the most recent of them. The table lists, for the topics of this page, what to do today.

Older practice Today

npm install -g express to get a command

The command comes from the separate express-generator package (npx express-generator), which is itself still an Express 4 scaffold; or skip the generator.

Scaffolds with Jade as the default view engine and var-style CommonJS

Jade is now Pug; pass --view=pug, --no-view, or write the layout by hand and use ESM.

app.listen() without an error path

In Express 5 the callback receives the error; see the error callback.

Restarting with nodemon and loading variables with dotenv in development

node --watch and node --env-file=.env cover both with no dependency; nodemon is not recommended in production.

Secrets and database credentials written in source files or committed .env files

Generate secrets, keep them in the environment, commit only .env.example.

app.use(bodyParser()) and the separate body-parser package

express.json() and express.urlencoded() are built in; req.body is undefined when no parser is registered.

res.send(status, body)

Removed in Express 5; use res.status(code).send(body).

Tests that import a module which calls app.listen()

Export the app from one module and listen in another, as above.

What changed recently

  • Express 5.0 (2024-09-10) removed deprecated methods, moved to path-to-regexp 8, forwarded rejected promises and passed the listen error to its callback. It was published on the next tag.

  • Express 5.1 (2025-03-31) became the first 5.x release on the latest tag, which is why a plain npm install express installs Express 5 today.

  • Express 5.2 (2025-12-01) and 5.3 (2026-10-09) are minor releases; 5.3 adds app.query() and updates the qs and proxy-addr dependencies for security fixes.

  • Express 4.22.3 is the latest 4.x release; version 4 stays on the latest-4 dist-tag.

  • The website was rebuilt in 2026. Documentation URLs now end with a slash; unversioned paths are the 5.x docs, and /en/4x/ and /en/3x/ hold older references.

  • Node.js gained --watch, --env-file and type stripping, which removed the need for nodemon, dotenv and a compile step in simple projects.