1. Quick Start
Express is a fast, unopinionated, minimalist web framework for Node.js. It provides a thin layer of fundamental web application features — routing, middleware, templating, and HTTP utilities — without obscuring the Node.js platform underneath.
Key features:
- Robust routing with support for parameters, pattern matching, and HTTP method handlers
- Middleware pipeline architecture for composable request/response processing
- Built-in body parsing (
json,urlencoded,raw,text) and static file serving - Template engine integration (EJS, Handlebars, Pug, and any engine exposing
__express) - Content negotiation via
Acceptheader inspection - Cookie handling with optional signing
- Proxy trust configuration for
X-Forwarded-*header support - Sub-application mounting for modular architectures
Prerequisites: Node.js 18 or later
Install and run:
npm install express
Minimal application:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
Server listening on port 3000
Tip
Pass an object or array to
res.send()and Express automatically serializes it as JSON with the correctContent-Typeheader — no need to callres.json()explicitly.
What to try next: see Configuration for application settings, Core Usage for routing and middleware, and Common Tasks for real-world recipes.
2. Installation
System Requirements
| Requirement | Minimum |
|---|---|
| Node.js | >= 18 |
| npm | Any version bundled with Node 18+ |
| OS | Any platform supported by Node.js |
Install Methods
npm
npm install express
yarn
yarn add express
pnpm
pnpm add express
Post-Install Verification
node -e "console.log(require('express/package.json').version)"
5.2.1
Important
Express 5 requires Node.js 18 or later. If you are running an older Node.js version, you must upgrade before installing.
Dependencies
Express 5.2.1 ships with these production dependencies (installed automatically):
| Dependency | Purpose |
|---|---|
body-parser ^2.2.1 |
Request body parsing (JSON, URL-encoded, raw, text) |
router ^2.2.0 |
HTTP routing engine |
serve-static ^2.2.0 |
Static file serving middleware |
send ^1.1.0 |
File streaming for res.sendFile |
accepts ^2.0.0 |
Content negotiation |
cookie ^0.7.1 |
Cookie parsing/serialization |
cookie-signature ^1.2.1 |
Signed cookie support |
qs ^6.14.2 |
Extended query string parsing |
proxy-addr ^2.0.7 |
Trusted proxy address resolution |
debug ^4.4.0 |
Debug logging (DEBUG=express:*) |
finalhandler ^2.1.0 |
Default error handling |
mime-types ^3.0.0 |
MIME type resolution |
etag ^1.8.1 |
ETag generation |
fresh ^2.0.0 |
HTTP cache freshness checks |
http-errors ^2.0.0 |
HTTP error creation |
depd ^2.0.0 |
Deprecation notices |
content-disposition ^1.0.0 |
Content-Disposition header generation |
content-type ^1.0.5 |
Content-Type header parsing |
encodeurl ^2.0.0 |
URL encoding |
escape-html ^1.0.3 |
HTML entity escaping |
merge-descriptors ^2.0.0 |
Object descriptor merging |
on-finished ^2.4.1 |
Response finish detection |
once ^1.4.0 |
One-time callback wrapper |
parseurl ^1.3.3 |
URL parsing with caching |
range-parser ^1.2.1 |
Range header parsing |
statuses ^2.0.1 |
HTTP status code utilities |
type-is ^2.0.1 |
Content-Type checking |
vary ^1.1.2 |
Vary header management |
Install Troubleshooting
Q: npm install fails with permission errors
Run npm install express without sudo. If your global node_modules has permission issues, configure npm to use a user-level prefix: npm config set prefix ~/.npm-global.
Q: Error: Cannot find module 'express'
Ensure you ran npm install in the project directory (where package.json lives), not globally.
Q: Module version conflicts with other packages
Delete node_modules and package-lock.json, then run npm install again to get a clean dependency tree.
3. Configuration
Express applications are configured via app.set(setting, value). Settings are inherited by mounted sub-apps.
Application Settings Reference
| Setting | Default | Values | Effect |
|---|---|---|---|
env |
NODE_ENV or 'development' |
Any string | Controls environment mode; 'production' enables view caching |
etag |
'weak' |
'weak', 'strong', false, or function |
ETag generation strategy |
jsonp callback name |
'callback' |
Any string | Query parameter name for JSONP responses |
json escape |
N/A | Boolean | Escape <, >, & in JSON responses |
json replacer |
N/A | Function | Custom JSON.stringify replacer |
json spaces |
N/A | Number | JSON pretty-print indentation |
query parser |
'simple' |
'simple', 'extended', false, or function |
Query string parser (simple = Node built-in, extended = qs library) |
subdomain offset |
2 |
Number | Number of dot-separated parts to remove for req.subdomains |
trust proxy |
false |
Boolean, string, number, array, or function | Trust X-Forwarded-* headers from proxies |
views |
./views (resolved from CWD) |
String or array of strings | Template view directory |
view cache |
true in production |
Boolean | Cache compiled template lookups |
view engine |
N/A | String (e.g., 'ejs', 'pug') |
Default template engine extension |
view |
View (built-in) |
Constructor | Custom view class |
x-powered-by |
true |
Boolean | Send X-Powered-By: Express header |
case sensitive routing |
false |
Boolean | Treat /Foo and /foo as different routes |
strict routing |
false |
Boolean | Treat /foo and /foo/ as different routes |
Warning
The
query parsersetting defaults to'simple'in Express 5 (using Node's built-inquerystringmodule), which does not support nested objects. If your application relies on nested query parameters like?user[name]=tobi, setapp.set('query parser', 'extended').
Common Configuration Patterns
const app = express();
// Production-ready baseline
app.set('trust proxy', 1); // trust first proxy hop
app.set('etag', 'strong'); // strong ETags for caching
app.set('query parser', 'extended'); // nested query object support
app.disable('x-powered-by'); // hide framework fingerprint
// Template engine setup
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
Environment Variable
| Variable | Purpose |
|---|---|
NODE_ENV |
Sets app.get('env'). When set to 'production', view caching is enabled automatically. |
Tip
Use
app.enable('setting')andapp.disable('setting')as shorthand forapp.set('setting', true/false). Check state withapp.enabled('setting')orapp.disabled('setting').
See the Admin Guide for full configuration reference with internals and compilation details.
4. Core Usage
Creating an Application
const express = require('express');
const app = express();
The express() factory returns a function that doubles as an HTTP request handler. The returned object inherits from EventEmitter and exposes the full application API.
Routing
Routes map HTTP methods and URL paths to handler functions.
Basic Route Methods
app.get('/users', (req, res) => {
res.json([{ name: 'Tobi' }]);
});
app.post('/users', (req, res) => {
res.status(201).json({ created: true });
});
app.put('/users/:id', (req, res) => {
res.json({ updated: req.params.id });
});
app.delete('/users/:id', (req, res) => {
res.sendStatus(204);
});
Express supports all HTTP methods recognized by Node.js (get, post, put, delete, patch, options, head, and more). Use app.all() to match every method on a path:
app.all('/secret', (req, res, next) => {
console.log('Accessing the secret section...');
next();
});
Route Parameters
app.get('/user/:id', (req, res) => {
res.send(`User ${req.params.id}`);
});
// Range parameters
app.get('/users/:from-:to', (req, res) => {
res.json({ from: req.params.from, to: req.params.to });
});
Route Objects
Use app.route() to chain handlers for a single path:
app.route('/book')
.get((req, res) => res.send('Get a book'))
.post((req, res) => res.send('Add a book'))
.put((req, res) => res.send('Update the book'));
Parameter Pre-Processing
Use app.param() to run logic before route handlers when a named parameter is present:
app.param('user', (req, res, next, id) => {
// load user from database
req.user = users[id];
next();
});
app.get('/user/:user', (req, res) => {
res.json(req.user);
});
Note
app.param()accepts an array of parameter names:app.param(['id', 'page'], callback).
Middleware
Middleware functions have access to the request, response, and the next function in the pipeline.
Application-Level Middleware
// Runs on every request
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
// Runs only for paths starting with /api
app.use('/api', (req, res, next) => {
// authenticate API requests
next();
});
Built-In Middleware
Express 5 ships five built-in middleware functions:
| Middleware | Usage | Purpose |
|---|---|---|
express.json() |
app.use(express.json()) |
Parse JSON request bodies |
express.urlencoded() |
app.use(express.urlencoded()) |
Parse URL-encoded form bodies |
express.raw() |
app.use(express.raw()) |
Parse raw binary request bodies |
express.text() |
app.use(express.text()) |
Parse plain text request bodies |
express.static(root) |
app.use(express.static('public')) |
Serve static files from a directory |
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
Important
Body parsers must be mounted before route handlers that read
req.body. Withoutexpress.json(),req.bodywill beundefinedfor JSON requests.
Error-Handling Middleware
Error-handling middleware takes four arguments. Express recognizes the 4-argument signature as an error handler:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
Warning
The error handler must have exactly four parameters
(err, req, res, next)even if you do not usenext. Omitting any parameter changes the function signature and Express will not recognize it as an error handler.
Request Object (req)
| Property/Method | Returns | Description |
|---|---|---|
req.params |
Object | Named route parameters |
req.query |
Object | Parsed query string (depends on query parser setting) |
req.body |
Any | Parsed request body (requires body-parser middleware) |
req.get(header) / req.header(header) |
String | Request header value (case-insensitive) |
req.is(type) |
String/false/null | Check if Content-Type matches type |
req.accepts(types) |
String/false | Content negotiation based on Accept header |
req.acceptsCharsets(...charsets) |
String/Array | Check Accept-Charset |
req.acceptsEncodings(...encodings) |
String/Array | Check Accept-Encoding |
req.acceptsLanguages(...langs) |
String/Array | Check Accept-Language |
req.range(size, [options]) |
Array/number | Parse Range header |
req.ip |
String | Remote IP (respects trust proxy) |
req.ips |
Array | Proxy chain IPs when trust proxy is set |
req.hostname |
String | Hostname from Host header (respects trust proxy) |
req.host |
String | Host header value (includes port if present) |
req.path |
String | URL pathname |
req.protocol |
String | 'http' or 'https' (respects trust proxy) |
req.secure |
Boolean | true if req.protocol === 'https' |
req.subdomains |
Array | Subdomain array (respects subdomain offset) |
req.fresh |
Boolean | Response cache is still valid |
req.stale |
Boolean | Response cache is expired |
req.xhr |
Boolean | true if X-Requested-With: XMLHttpRequest |
Response Object (res)
| Method | Description |
|---|---|
res.status(code) |
Set HTTP status code (chainable). Must be integer 100–999. |
res.send(body) |
Send response (string → HTML, object/array → JSON, Buffer → binary) |
res.json(obj) |
Send JSON response |
res.jsonp(obj) |
Send JSON with JSONP callback support |
res.sendStatus(code) |
Set status and send status message as body |
res.sendFile(path, [options], [callback]) |
Stream a file with Content-Type auto-detection |
res.download(path, [filename], [options], [callback]) |
Prompt file download (sets Content-Disposition) |
res.redirect([status], url) |
Redirect (default 302) |
res.render(view, [locals], [callback]) |
Render a template |
res.type(type) / res.contentType(type) |
Set Content-Type |
res.format(obj) |
Content negotiation response |
res.set(field, [value]) / res.header(field, [value]) |
Set response header(s) |
res.get(field) |
Get response header value |
res.append(field, value) |
Append to response header |
res.cookie(name, value, [options]) |
Set a cookie |
res.clearCookie(name, [options]) |
Clear a cookie |
res.location(url) |
Set Location header |
res.links(links) |
Set Link header |
res.vary(field) |
Add Vary header |
res.locals |
Object |
Template Rendering
app.set('view engine', 'ejs');
app.set('views', './views');
app.get('/', (req, res) => {
res.render('index', { title: 'Home', users: ['Tobi', 'Loki'] });
});
Register custom engine mappings with app.engine():
app.engine('html', require('ejs').renderFile);
Note
Express auto-requires template engines by file extension. For
.ejsfiles, it callsrequire('ejs').__express. Useapp.engine()only when mapping a non-standard extension or when the engine lacks__express.
Sub-Applications (Mounting)
Mount an Express app as middleware on another app:
const admin = express();
admin.get('/', (req, res) => {
res.send('Admin homepage');
});
const app = express();
app.use('/admin', admin);
Mounted apps inherit trust proxy, request/response prototypes, engines, and settings from their parent. The mount event fires on the child app when mounted. admin.mountpath returns '/admin'.
The Express Router
Use express.Router() to create modular, mountable route handlers:
const router = express.Router();
router.get('/', (req, res) => {
res.json({ version: 1 });
});
router.get('/users', (req, res) => {
res.json([]);
});
app.use('/api/v1', router);
5. Common Tasks
Serve Static Files
app.use(express.static('public'));
// Files in ./public/ are now served at the root URL
// e.g., public/css/style.css → GET /css/style.css
Mount at a virtual path prefix:
app.use('/static', express.static('public'));
// public/css/style.css → GET /static/css/style.css
Parse JSON Request Bodies
app.use(express.json());
app.post('/api/data', (req, res) => {
console.log(req.body); // parsed JSON object
res.json({ received: true });
});
Handle Form Submissions
app.use(express.urlencoded({ extended: true }));
app.post('/login', (req, res) => {
const { username, password } = req.body;
// authenticate...
res.redirect('/dashboard');
});
Set and Read Cookies
app.get('/', (req, res) => {
res.cookie('rememberme', '1', {
maxAge: 900000,
httpOnly: true
});
res.send('Cookie set');
});
To clear a cookie:
res.clearCookie('rememberme', { path: '/' });
Tip
The
maxAgeoption is in milliseconds and automatically generates anexpiresdate. Express convertsmaxAgeto seconds internally for theSet-Cookieheader.
File Downloads
app.get('/download', (req, res) => {
res.download('/path/to/report.pdf', 'report.pdf', (err) => {
if (err) {
// handle error, but check if headers were already sent
if (!res.headersSent) {
res.status(500).send('Download failed');
}
}
});
});
Content Negotiation
app.get('/data', (req, res) => {
res.format({
'text/html': () => {
res.send('<p>Hello</p>');
},
'application/json': () => {
res.json({ message: 'Hello' });
},
'text/plain': () => {
res.send('Hello');
},
default: () => {
res.status(406).send('Not Acceptable');
}
});
});
Build a REST API
const express = require('express');
const app = express();
app.use(express.json());
const users = [
{ id: 1, name: 'Tobi' },
{ id: 2, name: 'Loki' }
];
app.get('/api/users', (req, res) => {
res.json(users);
});
app.get('/api/user/:name/repos', (req, res) => {
const user = users.find(u => u.name === req.params.name);
if (!user) return res.sendStatus(404);
res.json({ user: user.name, repos: [] });
});
app.listen(3000);
Session Management
const session = require('express-session');
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: false
}));
app.get('/login', (req, res) => {
req.session.user = { name: 'tobi' };
res.send('Logged in');
});
Virtual Hosts
const vhost = require('vhost');
const api = express();
api.get('/', (req, res) => res.send('API'));
app.use(vhost('api.example.com', api));
Multi-Router Architecture
const apiV1 = express.Router();
apiV1.get('/users', (req, res) => res.json({ version: 1 }));
const apiV2 = express.Router();
apiV2.get('/users', (req, res) => res.json({ version: 2 }));
app.use('/api/v1', apiV1);
app.use('/api/v2', apiV2);
Custom Error Pages
// 404 handler (after all routes)
app.use((req, res) => {
res.status(404).render('404', { url: req.originalUrl });
});
// Error handler
app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status);
if (app.get('env') === 'development') {
res.render('500', { error: err });
} else {
res.render('500', { error: {} });
}
});
HTTPS Server
const https = require('node:https');
const fs = require('node:fs');
const express = require('express');
const app = express();
const options = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.cert')
};
https.createServer(options, app).listen(443);
Note
app.listen()creates an HTTP server internally. For HTTPS, create the server withhttps.createServer()and pass the Express app as the callback.
User Authentication Pattern
function restrict(req, res, next) {
if (req.session.user) {
next();
} else {
req.session.error = 'Access denied!';
res.redirect('/login');
}
}
app.get('/restricted', restrict, (req, res) => {
res.send('Protected content');
});
Online User Tracking
let onlineCount = 0;
app.use((req, res, next) => {
if (!req.session.online) {
req.session.online = true;
onlineCount++;
}
next();
});
app.get('/', (req, res) => {
res.send(`${onlineCount} users online`);
});
6. Troubleshooting
Diagnostic Decision Tree
- App won't start? → Check Node.js version (
node -vmust be >= 18) and thatexpressis innode_modules - Route not matching? → Check method, path, and middleware order. Routes are matched in registration order.
req.bodyis undefined? → Mountexpress.json()orexpress.urlencoded()before your routes- Static files not serving? → Verify the directory path passed to
express.static()is correct relative to where you runnode - Template not found? → Check
viewssetting andview enginesetting
Error: app.use() requires a middleware function
Cause: You passed a non-function argument to app.use() without a path prefix, or passed undefined (often from a missing/misspelled require).
Solution:
- Verify the middleware module is installed:
npm ls <module-name> - Check spelling of the
require()call - Ensure the module exports a function
Prevention: Always verify that imported middleware is a function before passing it to app.use().
Error: callback function required
Cause: app.engine() was called with a non-function second argument.
Solution:
// Wrong
app.engine('html', 'ejs');
// Correct
app.engine('html', require('ejs').renderFile);
Error: No default engine was specified and no extension was provided.
Cause: res.render('viewname') was called without setting a view engine and without providing a file extension.
Solution:
// Option 1: Set default engine
app.set('view engine', 'ejs');
res.render('index'); // looks for views/index.ejs
// Option 2: Provide extension explicitly
res.render('index.ejs');
Error: Failed to lookup view "..." in views directory "..."
Cause: The template file does not exist in the configured views directory.
Solution:
- Check that
app.set('views', path)points to the correct directory - Verify the template file exists with the correct extension
- If using multiple view directories, pass an array:
app.set('views', [dir1, dir2])
Error: path must be absolute or specify root to res.sendFile
Cause: res.sendFile() received a relative path without the root option.
Solution:
// Option 1: Absolute path
res.sendFile(path.join(__dirname, 'files', 'report.pdf'));
// Option 2: Use root option
res.sendFile('report.pdf', { root: path.join(__dirname, 'files') });
Error: name argument is required to req.get
Cause: req.get() or req.header() was called without a header name argument, or the argument was not a string.
Solution: Always pass a string: req.get('Content-Type').
Error: Invalid status code: ... Status code must be an integer.
Cause: res.status() received a non-integer value (string, float, etc.).
Solution:
// Wrong
res.status('200');
res.status(200.5);
// Correct
res.status(200);
Prevention: Always pass integer literals to res.status().
Error: cookieParser("secret") required for signed cookies
Cause: res.cookie() was called with { signed: true } but no cookie-parser middleware with a secret is registered.
Solution:
const cookieParser = require('cookie-parser');
app.use(cookieParser('my-secret'));
res.cookie('name', 'value', { signed: true });
Warning
Signed cookies require the
cookie-parsermiddleware with a secret string. Without it, Express throws at response time, not at startup — this error only appears when a handler actually tries to set a signed cookie.
Error: Content-Type cannot be set to an Array
Cause: res.set('Content-Type', [...]) was called with an array value, which is not supported for the Content-Type header.
Solution: Pass a single string: res.set('Content-Type', 'application/json').
Error: unknown value for query parser function: ...
Cause: app.set('query parser', value) received an unsupported value.
Solution: Use one of the supported values: 'simple', 'extended', false, or a custom function.
Issue: BigInt values cause cryptic TypeError with res.json() (#7186)
Cause: JSON.stringify() does not support BigInt natively. Passing an object containing BigInt values to res.json() throws a TypeError without a clear message.
Solution: Convert BigInt values to strings or numbers before sending:
res.json({ id: bigintValue.toString() });
Issue: Single-item array query parameter not parsed correctly (#7178)
Cause: When using the default 'simple' query parser, single-element arrays in query strings may not parse as expected, particularly behind certain hosting platforms.
Solution: Switch to the 'extended' query parser: app.set('query parser', 'extended').
Caution
When deploying behind reverse proxies (nginx, cloud load balancers), always configure
trust proxycorrectly. An incorrect setting causesreq.ip,req.protocol,req.hostname, andreq.secureto return wrong values, potentially breaking redirects, HTTPS detection, and rate limiting.
Issue: Mounting at '//' does not work as expected (#4557)
Cause: app.use('//', handler) does not mount at the double-slash path as one might expect.
Solution: Avoid double-slash mount paths. Use single-slash paths and handle URL normalization upstream in your reverse proxy.
Debugging
Enable Express debug logs by setting the DEBUG environment variable:
DEBUG=express:* node app.js
express:application booting in development mode +0ms
express:router use / query +2ms
express:router:route new / +0ms
Narrow debug output to specific modules:
| Pattern | Shows |
|---|---|
express:application |
App initialization and settings |
express:router |
Route matching and dispatch |
express:view |
Template lookup and rendering |
7. FAQ
Q: How do I structure a large Express application?
Use the Router to split routes into separate files by feature or resource. Each file exports a router, and the main app mounts them with app.use('/prefix', router). See the examples/route-separation and examples/multi-router patterns included in the Express repository.
Q: How do I handle async/await errors in route handlers?
Express 5 automatically catches rejected promises from async route handlers and forwards the error to your error-handling middleware. You do not need a try/catch wrapper or a separate asyncHandler utility.
Q: Can I use Express with TypeScript?
Yes. Install @types/express for type definitions: npm install --save-dev @types/express. Express itself is written in JavaScript, but TypeScript typings are maintained by the DefinitelyTyped community.
Q: How do I disable the X-Powered-By header?
app.disable('x-powered-by');
Q: What is the difference between res.send() and res.json()?
res.send() auto-detects the body type: strings are sent as HTML, objects/arrays are serialized as JSON (delegating to res.json()), and Buffers are sent as binary. res.json() always serializes as JSON and respects json replacer, json spaces, and json escape settings.
Q: How do I serve an SPA (Single-Page Application) with Express?
Serve static assets with express.static(), then add a catch-all route that sends your index.html:
app.use(express.static('dist'));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
Q: How do I redirect HTTP to HTTPS?
app.set('trust proxy', 1);
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(301, `https://${req.hostname}${req.originalUrl}`);
}
next();
});
Q: How do I set cookies that cannot be accessed by client-side JavaScript?
Use the httpOnly option: res.cookie('session', token, { httpOnly: true, secure: true }).
Q: What query parser should I use?
Use 'simple' (default) for flat key=value pairs. Use 'extended' if your frontend sends nested objects (user[name]=tobi) or arrays (colors[]=red&colors[]=blue).
Q: Does app.get() handle settings and routes?
Yes. With one argument, app.get('setting') returns a setting value. With two or more arguments, app.get('/path', handler) defines a GET route. This dual behavior is intentional.
Q: How do I handle CORS?
Use the cors middleware package (npm install cors) or set headers manually:
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.set('Access-Control-Allow-Headers', 'Content-Type');
next();
});