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 |
|
18.11 (stable since 20.13 and 22) |
Restarts the process when an imported file changes. |
|
20.6 |
Loads variables from a file into |
Running |
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. |
|
a line with HTTP |
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
Source: Express: Installing
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
Source: Express: Installing
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}`);
});
Source: Express: Hello world
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!
Source: Express: 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}`);
});
Source: Express: Hello world
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
Source: Express: Installing - TypeScript
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}`);
});
Source: Express: app.listen(), Express: res.json()
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 |
|---|---|
|
The TCP port. Omitted or |
|
The address to bind. Omit it to listen on all interfaces; use |
|
The maximum length of the queue of pending connections. |
|
A UNIX socket path, as an alternative to host and port. |
|
Called once the server is listening. In Express 5 it also receives the |
Source: Express: app.listen()
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())}`);
});
Source: Express: app.listen()
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 thehttp.Server. Keep it: tests, graceful shutdown and WebSocket libraries need it. -
Code that must run on both lines can attach an
errorlistener 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.
-
--watchrestarts the process when the entry file or any module it imports changes.--watch-path=./srcwatches a specific directory instead. -
--env-file=.envreadsKEY=valuelines from a file intoprocess.envbefore 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:
-
--watchis a development tool. In production runnode app.jsunder a process manager, a container orchestrator or systemd, and let that supervisor restart it. -
--env-filedoes 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=productionin 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 |
|
How do I define models? |
Express has no model concept or database layer. Use any Node.js database module. |
|
How can I authenticate users? |
Express prescribes no scheme; the |
|
Which template engines does Express support? |
Any engine that follows the |
|
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. |
|
How do I set up an error handler? |
Define middleware with four parameters: |
|
How do I render plain HTML? |
You do not need |
|
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
Source: Express: Application generator
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 |
|---|---|
|
Print the usage. |
|
Print the generator version. |
|
Add EJS support. |
|
Add Handlebars support. |
|
Add Pug support. |
|
Add Hogan.js support. |
|
Generate without a view engine (an API-only skeleton). |
|
Choose |
|
Choose a stylesheet engine: |
|
Add a |
|
Generate into a directory that is not empty. |
Source: Express: Application generator
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
Source: Express: Application generator
The responsibilities are split like this:
-
bin/wwwcreates the HTTP server, picks the port (PORTor 3000), registers theerrorandlisteninglisteners and starts listening. It is the filenpm startruns. This is the app/server split that tests need, done with a script instead of a module. -
app.jsbuilds 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.jsandusers.jsare placeholders). -
views/andpublic/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.jsonpins roughly the 4.16 series),morgan,cookie-parser,debugandhttp-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=pugor another engine, or--no-viewfor an API. -
The code style is that of 2018:
var, CommonJS,functionexpressions, a hand-writtennormalizePortand anonErrorfunction that handlesEACCESandEADDRINUSEinbin/www. -
The package name is
express-generator; since Express 4,npm install -g expressno longer installs a command, because theexpressbinary moved out of theexpresspackage.
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.
-
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 -
Switch to ESM: add the
typefield set tomoduletopackage.json, rename nothing, and convertrequiretoimportandmodule.exportstoexport default. Add.jsextensions to relative imports and replace__dirnamewithimport.meta.dirname(Node.js 20.11 and later). -
Replace
varwithconstorletand callbacks withasyncfunctions. Express 5 forwards a rejected promise of a handler to the error handlers, so thetry/catchandnext(err)wrapper is no longer needed. -
Delete
bin/wwwand move its job to a smallserver.jsthat imports the app and uses theapp.listen()error callback from above. -
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. -
Set
extended: trueonexpress.urlencoded()only if you need nested form fields; the default is nowfalse. -
Move secrets and ports to the environment and run with
node --watch --env-file=.envin 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"
}
}
Source: Express: Application generator, Express: Migrating to Express 5, Node.js: import.meta.dirname
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 |
|---|---|---|
|
A simple request handler |
Hello world above |
|
Organising routes per resource, with a map, and with MVC-style controllers |
|
|
Multiple operations on one resource, route parameters, route-level middleware |
|
|
Several routers, virtual hosts |
|
|
Error middleware, custom error pages |
|
|
Serving static files, sending files to the client |
|
|
Template engines and views, rendering dynamically, data kept on the request across middleware |
|
|
Cookies, cookie sessions, server sessions, activity tracked in Redis |
|
|
Authentication with login and password |
|
|
Content negotiation, a simple API service, a search API |
Source: Express: Examples
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 |
|---|---|
|
The command comes from the separate |
Scaffolds with Jade as the default view engine and |
Jade is now Pug; pass |
|
In Express 5 the callback receives the |
Restarting with |
|
Secrets and database credentials written in source files or committed |
Generate secrets, keep them in the environment, commit only |
|
|
|
Removed in Express 5; use |
Tests that import a module which calls |
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
listenerror to its callback. It was published on thenexttag. -
Express 5.1 (2025-03-31) became the first 5.x release on the
latesttag, which is why a plainnpm install expressinstalls 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 theqsandproxy-addrdependencies for security fixes. -
Express 4.22.3 is the latest 4.x release; version 4 stays on the
latest-4dist-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-fileand type stripping, which removed the need fornodemon,dotenvand a compile step in simple projects.