1. Architecture Overview
Express is a minimal, unopinionated web framework for Node.js that provides a thin abstraction over Node's built-in http module. It implements a middleware pipeline architecture where incoming HTTP requests flow through an ordered chain of handler functions, each with the ability to modify the request/response objects, short-circuit the pipeline, or delegate to the next handler via next().
The framework consists of five core modules totaling approximately 62K characters of source code, plus a View class for template rendering. Express delegates routing to the external router package (v2.2.0) and body parsing to body-parser (v2.2.1), keeping the core focused on application lifecycle, request/response augmentation, and middleware orchestration.
Component Architecture
express()
│
┌───────┴───────┐
│ Application │ (lib/application.js)
│ EventEmitter │
│ mixin │
└───────┬───────┘
│
┌─────────────┼─────────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌─────┴─────┐
│ Request │ │Response │ │ Router │
│ proto │ │ proto │ │ (external)│
│ (req) │ │ (res) │ │ │
└─────────┘ └─────────┘ └──────┬─────┘
│
┌─────┴─────┐
│ Route │
│ (external)│
└───────────┘
┌─────────┐
│ View │ (lib/view.js)
└─────────┘
Built-in Middleware (re-exported from dependencies):
├── express.json() ← body-parser
├── express.urlencoded() ← body-parser
├── express.raw() ← body-parser
├── express.text() ← body-parser
└── express.static() ← serve-static
Component Inventory
| Component | Path | Role |
|---|---|---|
| Entry point | index.js |
Re-exports lib/express.js |
| Factory + Exports | lib/express.js |
createApplication() factory; exposes Router, Route, middleware |
| Application | lib/application.js |
App prototype: settings, routing delegation, listen(), render() |
| Request | lib/request.js |
Extended IncomingMessage prototype: query, ip, accepts, is |
| Response | lib/response.js |
Extended ServerResponse prototype: send, json, redirect, render |
| Utilities | lib/utils.js |
ETag compilation, query parser compilation, trust proxy compilation, MIME normalization |
| View | lib/view.js |
Template engine lookup, caching, and rendering |
Technology Stack
| Layer | Technology |
|---|---|
| Runtime | Node.js >= 18 |
| HTTP server | Node.js http.createServer() |
| Routing | router v2.2.0 (external package) |
| Body parsing | body-parser v2.2.1 |
| Static files | serve-static v2.2.0 |
| File streaming | send v1.1.0 |
| Cookie handling | cookie v0.7.1 + cookie-signature v1.2.1 |
| Content negotiation | accepts v2.0.0 |
| Query parsing | qs v6.14.2 (extended mode) / Node querystring (simple mode) |
| Proxy trust | proxy-addr v2.0.7 |
| ETag generation | etag v1.8.1 |
| Cache freshness | fresh v2.0.0 |
| Debug logging | debug v4.4.0 |
Data Flow
Client Request
│
▼
http.createServer(app)
│
▼
app.handle(req, res)
├── Set X-Powered-By header (if enabled)
├── Set req.res / res.req cross-references
├── Set prototypes: req → app.request, res → app.response
├── Initialize res.locals
│
▼
Router.handle(req, res, finalhandler)
├── Match route by method + path
├── Execute middleware stack in order
├── Parameter pre-processing (app.param callbacks)
│
▼
Route Handler
├── Read req.params, req.query, req.body
├── Build response via res.send/json/render/redirect
│
▼
finalhandler (if no route matched or error)
└── 404 or error response
Note
The Router is lazily instantiated on first use (first
app.use(),app.get(), etc.). This means an Express app with no routes registered never allocates a Router instance.
2. System Requirements
| Requirement | Detail |
|---|---|
| Node.js | >= 18 (specified in package.json engines field) |
| npm | Any version bundled with supported Node.js |
| Disk | Minimal — express + dependencies total ~1.5 MB in node_modules |
| Memory | Framework overhead is negligible; memory usage depends on application middleware and data |
| OS | All platforms supported by Node.js (Linux, macOS, Windows, etc.) |
Production Dependencies (27 packages)
| Package | Version | Purpose |
|---|---|---|
accepts |
^2.0.0 | Content negotiation |
body-parser |
^2.2.1 | Request body parsing |
content-disposition |
^1.0.0 | Content-Disposition header |
content-type |
^1.0.5 | Content-Type parsing |
cookie |
^0.7.1 | Cookie serialization |
cookie-signature |
^1.2.1 | Cookie signing |
debug |
^4.4.0 | Debug logging |
depd |
^2.0.0 | Deprecation notices |
encodeurl |
^2.0.0 | URL encoding |
escape-html |
^1.0.3 | HTML escaping |
etag |
^1.8.1 | ETag generation |
finalhandler |
^2.1.0 | Default error handler |
fresh |
^2.0.0 | Freshness checking |
http-errors |
^2.0.0 | HTTP error objects |
merge-descriptors |
^2.0.0 | Property descriptor merging |
mime-types |
^3.0.0 | MIME type lookup |
on-finished |
^2.4.1 | Response finish event |
once |
^1.4.0 | One-time function wrapper |
parseurl |
^1.3.3 | URL parsing with caching |
proxy-addr |
^2.0.7 | Client address determination |
qs |
^6.14.2 | Extended query string parser |
range-parser |
^1.2.1 | Range header parsing |
router |
^2.2.0 | HTTP routing |
send |
^1.1.0 | Static file streaming |
serve-static |
^2.2.0 | Static file middleware |
statuses |
^2.0.1 | HTTP status codes |
type-is |
^2.0.1 | Content-Type checking |
vary |
^1.1.2 | Vary header management |
Development Dependencies
| Package | Version | Purpose |
|---|---|---|
mocha |
^10.7.3 | Test runner |
supertest |
^6.3.0 | HTTP assertion library |
nyc |
^17.1.0 | Code coverage |
eslint |
8.47.0 | Linting |
ejs |
^3.1.10 | Test template engine |
hbs |
4.2.0 | Handlebars test engine |
marked |
^15.0.3 | Markdown test engine |
express-session |
^1.18.1 | Session example/tests |
connect-redis |
^8.0.1 | Redis session example |
cookie-parser |
1.4.7 | Cookie parsing tests |
cookie-session |
2.1.1 | Cookie session example |
method-override |
3.0.0 | HTTP method override tests |
morgan |
1.10.1 | HTTP logger example |
pbkdf2-password |
1.2.1 | Auth example |
vhost |
~3.0.2 | Virtual host example |
after |
0.8.2 | Test utility |
3. Configuration Reference
Application Settings (Complete)
Settings are managed via app.set(key, value) / app.get(key), with convenience methods app.enable(key), app.disable(key), app.enabled(key), app.disabled(key).
| Setting | Type | Default | Description | Compiled? |
|---|---|---|---|---|
env |
String | process.env.NODE_ENV or 'development' |
Environment mode | No |
etag |
String/Boolean/Function | 'weak' |
ETag generation. 'weak' = weak validator, 'strong' = strong, false = disabled, function = custom |
Yes → etag fn |
jsonp callback name |
String | 'callback' |
Query parameter for JSONP callback name | No |
json escape |
Boolean | undefined | Escape <, >, & in res.json() / res.jsonp() output |
No |
json replacer |
Function | undefined | Custom replacer for JSON.stringify() |
No |
json spaces |
Number | undefined | Indentation spaces for JSON.stringify() |
No |
query parser |
String/Boolean/Function | 'simple' |
'simple' = querystring.parse, 'extended' = qs.parse with allowPrototypes, false = disabled, function = custom |
Yes → query parser fn |
subdomain offset |
Number | 2 |
Number of dot-delimited parts of the host to remove to access subdomains | No |
trust proxy |
Boolean/String/Number/Array/Function | false |
Proxy trust configuration for req.ip, req.hostname, req.protocol |
Yes → trust proxy fn |
views |
String/Array | resolve('views') |
Template file lookup directory/directories | No |
view cache |
Boolean | true when env === 'production' |
Cache resolved view paths | No |
view engine |
String | undefined | Default template engine extension (e.g., 'ejs') |
No |
view |
Constructor | Built-in View class |
View constructor for template resolution | No |
x-powered-by |
Boolean | true |
Add X-Powered-By: Express response header |
No |
case sensitive routing |
Boolean | false |
Case-sensitive route matching | No (passed to Router constructor) |
strict routing |
Boolean | false |
Strict slash handling (/foo ≠ /foo/) |
No (passed to Router constructor) |
Important
Three settings trigger compilation when set:
etag,query parser, andtrust proxy. Setting these stores both the raw value and a compiled function (e.g.,etag fn). The compiled function is what the framework actually calls at runtime. Setting these to invalid values throws aTypeError.
Compiled Setting Details
etag compilation (compileETag in lib/utils.js):
| Input | Compiled Function |
|---|---|
'weak' or true |
exports.wetag — weak ETag via etag package |
'strong' |
exports.etag — strong ETag via etag package |
false |
undefined (no ETag generation) |
| Function | Used as-is |
| Other | Throws TypeError: unknown value for etag function: <val> |
query parser compilation (compileQueryParser in lib/utils.js):
| Input | Compiled Function |
|---|---|
'simple' or true |
querystring.parse (Node built-in) |
'extended' |
qs.parse with { allowPrototypes: true } |
false |
undefined (query returns empty object) |
| Function | Used as-is |
| Other | Throws TypeError: unknown value for query parser function: <val> |
trust proxy compilation (compileTrust in lib/utils.js):
| Input | Compiled Function |
|---|---|
true |
Always returns true (trust all) |
false |
proxyaddr.compile([]) (trust none) |
Number n |
Trust first n hops: (addr, i) => i < n |
| String | Split on commas, pass to proxyaddr.compile() |
| Array | Pass directly to proxyaddr.compile() |
| Function | Used as-is |
Warning
Setting
trust proxytotruemeans Express trusts every proxy in the chain. In production, use a number (hop count) or specific IP addresses/subnets to limit trust.
Environment Variables
| Variable | Read By | Effect |
|---|---|---|
NODE_ENV |
app.defaultConfiguration() |
Sets app.get('env'). When 'production': enables view caching, suppresses error stack traces in finalhandler |
DEBUG |
debug package |
Enables debug logging. Use express:* for all Express debug output |
NO_DEPRECATION |
depd package |
Suppress deprecation warnings |
Setting Inheritance
When a sub-app is mounted via app.use(path, subApp), the child's mount event fires and the child inherits from the parent:
this.settingsprototype →parent.settings(settings chain viaObject.setPrototypeOf)this.enginesprototype →parent.enginesthis.requestprototype →parent.requestthis.responseprototype →parent.responsetrust proxyis inherited if the child still has the default value
4. Deployment & Scaling
Basic Production Deployment
const express = require('express');
const app = express();
// Production settings
app.set('env', 'production');
app.set('trust proxy', 1);
app.disable('x-powered-by');
app.listen(process.env.PORT || 3000);
Running Behind a Reverse Proxy
Express is typically deployed behind nginx, HAProxy, or a cloud load balancer. Configure trust proxy to correctly resolve client IP and protocol:
// Trust one proxy hop (most common)
app.set('trust proxy', 1);
// Trust specific proxy IPs
app.set('trust proxy', 'loopback, 10.0.0.0/8');
// Trust a subnet
app.set('trust proxy', ['10.0.0.0/8', '172.16.0.0/12']);
Tip
Use
app.set('trust proxy', 1)for single-proxy setups (the most common production configuration). This trusts only the first hop, usingX-Forwarded-For,X-Forwarded-Proto, andX-Forwarded-Hostheaders from that proxy.
Clustering
Express itself is single-threaded. Use Node's cluster module or a process manager to scale across CPU cores:
const cluster = require('node:cluster');
const os = require('node:os');
if (cluster.isPrimary) {
const cpus = os.cpus().length;
for (let i = 0; i < cpus; i++) {
cluster.fork();
}
} else {
const app = require('./app');
app.listen(3000);
}
Docker Deployment
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
USER node
CMD ["node", "app.js"]
Note
Express has no native Docker integration. The framework runs as a standard Node.js process inside any container.
Process Managers
| Manager | Use Case |
|---|---|
| PM2 | Production process management with clustering, monitoring, log management |
| systemd | Linux service management |
| Docker + orchestrator | Container-based deployment (Kubernetes, ECS, etc.) |
5. Operations
Health Checks
Express does not include a built-in health endpoint. Implement one:
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', uptime: process.uptime() });
});
Logging
Express has no built-in request logger. Use the morgan middleware (included as a dev dependency):
const morgan = require('morgan');
app.use(morgan('combined')); // Apache combined log format
Tip
In production, pipe morgan output to a log aggregation system rather than stdout. Use
morgan('combined', { stream: accessLogStream })with a write stream.
Debug Logging
Express uses the debug package with the express: namespace:
# All Express debug output
DEBUG=express:* node app.js
# Application-level only
DEBUG=express:application node app.js
# Router-level only
DEBUG=express:router node app.js
# View resolution only
DEBUG=express:view node app.js
Graceful Shutdown
const server = app.listen(3000);
process.on('SIGTERM', () => {
server.close(() => {
console.log('Server shut down gracefully');
process.exit(0);
});
});
Note
app.listen()returns a standardhttp.Serverinstance. Useserver.close()for graceful shutdown — it stops accepting new connections and waits for existing connections to complete.
Monitoring
Key metrics to monitor for Express applications:
| Metric | How to Collect |
|---|---|
| Request rate | Custom middleware counter or APM tool |
| Response latency | morgan response-time token or custom middleware using process.hrtime() |
| Error rate | Error-handling middleware counter |
| Event loop lag | perf_hooks.monitorEventLoopDelay() |
| Memory usage | process.memoryUsage() |
| Active connections | server.getConnections() |
6. Performance
ETag Optimization
Express generates ETags for every response by default (weak ETags). This enables client-side caching with 304 Not Modified responses. The freshness check (req.fresh) compares If-None-Match / If-Modified-Since headers against the response's ETag / Last-Modified.
| ETag Mode | Trade-off |
|---|---|
'weak' (default) |
Good balance — allows semantic equivalence |
'strong' |
Byte-for-byte match required — stricter but compatible with range requests |
false |
Disables ETag generation — saves CPU for responses where caching is not useful |
View Caching
In production (NODE_ENV=production), Express caches resolved view paths. The first res.render('index') call resolves the full filesystem path; subsequent calls use the cached path, skipping fs.statSync lookups.
This is automatic. To force caching in development for benchmarking:
app.enable('view cache');
JSON Serialization
For high-throughput JSON APIs, the json escape setting adds overhead by replacing <, >, & characters. Only enable it when embedding JSON in HTML contexts:
// Only if you serve JSON inside <script> tags
app.set('json escape', true);
Static File Serving
express.static() delegates to serve-static, which uses send for file streaming with:
fs.createReadStream()for efficient file transfer- Automatic
Content-Typefrom file extension ETagandLast-Modifiedheaders for cachingRangerequest support for partial content
For production, consider serving static files directly from nginx or a CDN rather than through Express.
7. Security
X-Powered-By Header
Express sends X-Powered-By: Express by default. This reveals the framework to attackers. Disable it in production:
app.disable('x-powered-by');
Caution
Leaving
x-powered-byenabled discloses your technology stack. While security-through-obscurity alone is insufficient, removing framework fingerprints is a low-cost hardening measure.
Trust Proxy Misconfiguration
Incorrect trust proxy settings can allow attackers to spoof their IP address via the X-Forwarded-For header:
trust proxy: truetrusts all proxy headers — an attacker can setX-Forwarded-For: 127.0.0.1and bypass IP-based access controlstrust proxy: false(default) ignores forwarded headers — safe butreq.ipreturns the proxy's IP, not the client's
Recommended: set to the exact number of proxy hops or specific proxy IP addresses.
JSONP Callback Sanitization
res.jsonp() sanitizes the callback parameter by stripping characters outside [\w$.\[\]] to prevent XSS injection through callback names. It also sets X-Content-Type-Options: nosniff to prevent MIME-type sniffing.
The response wraps JSON in a Rosetta Flash mitigation pattern:
/**/ typeof callback === 'function' && callback({"data":"value"});
Cookie Security
res.cookie()defaultspathto'/'if not specifiedmaxAgeis in milliseconds (converted to seconds in theSet-Cookieheader)- Signed cookies require
cookie-parsermiddleware with a secret - Object values are serialized as
j:+ JSON - Signed values are prefixed with
s:+ HMAC signature
Warning
The cookie
pathdefaults to'/', meaning cookies are sent on all paths by default. For sensitive cookies (e.g., admin sessions), restrict the path to the relevant route prefix.
JSON Escape
When json escape is enabled, res.json() and res.jsonp() replace < (\u003c), > (\u003e), and & (\u0026) in JSON output. This prevents HTML injection when JSON is embedded in <script> tags.
Redirect Security
res.redirect() escapes HTML entities in the redirect URL when generating the HTML response body, preventing XSS through redirect targets. The Location header value is URL-encoded via encodeUrl().
Warning
res.location()does not validate the URL. A redirect to user-supplied input without validation enables open redirect attacks. Always validate redirect targets against an allowlist.
View Path Traversal
The View.prototype.lookup() method resolves template paths within the configured views directory using path.resolve(). However, it does not enforce path containment — a view name like ../../etc/passwd could escape the views directory. GitHub issue #7140 tracks this as a known gap (unlike send, which enforces containment for res.sendFile()).
Caution
Never pass unsanitized user input as a view name to
res.render(). Always validate or whitelist template names.
Request Size Limits
The body-parser middleware (express.json(), express.urlencoded()) defaults to a 100KB request body limit. Configure it for your needs:
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ limit: '10mb', extended: true }));
Query String Prototype Pollution
The extended query parser passes { allowPrototypes: true } to qs.parse(). This prevents qs from stripping properties inherited from Object.prototype in parsed query objects, which can be a concern in code that uses hasOwnProperty checks or iterates over query parameters without guarding against prototype properties.
8. Developer Internals
Repository Structure
expressjs/express/
├── index.js # Entry point: re-exports lib/express.js
├── lib/
│ ├── express.js # createApplication() factory, middleware re-exports
│ ├── application.js # App prototype (settings, routing, render, listen)
│ ├── request.js # Request prototype extensions
│ ├── response.js # Response prototype extensions
│ ├── utils.js # Internal utilities (ETag/query/trust compilation)
│ └── view.js # View class (template lookup + render)
├── examples/ # 20+ example applications
│ ├── hello-world/ # Minimal Express app
│ ├── auth/ # Session-based authentication
│ ├── mvc/ # MVC pattern with controller autoloading
│ ├── multi-router/ # API versioning with Router
│ ├── route-separation/ # Route file organization
│ ├── content-negotiation/ # Accept header handling
│ ├── web-service/ # REST API example
│ ├── error-pages/ # Custom error pages (404/500)
│ ├── static-files/ # Static file serving
│ ├── session/ # Sessions (in-memory + Redis)
│ ├── cookies/ # Cookie handling
│ ├── cookie-sessions/ # Cookie-based sessions
│ ├── downloads/ # File download
│ ├── ejs/ # EJS template engine
│ ├── markdown/ # Markdown rendering
│ ├── params/ # Route parameters
│ ├── resource/ # Resource-oriented routing
│ ├── route-map/ # Route mapping pattern
│ ├── route-middleware/ # Per-route middleware
│ ├── search/ # Search with AJAX
│ ├── vhost/ # Virtual hosts
│ ├── view-constructor/ # Custom view class
│ ├── view-locals/ # Template local variables
│ ├── online/ # Online user counter
│ └── error/ # Error handling
├── test/ # Test suite
│ ├── app.*.js # Application tests
│ ├── req.*.js # Request property/method tests
│ ├── res.*.js # Response method tests
│ ├── express.*.js # Built-in middleware tests
│ ├── acceptance/ # Integration tests for examples
│ ├── support/ # Test utilities
│ └── fixtures/ # Test fixture files
├── package.json # Package manifest
├── .eslintrc.yml # ESLint configuration
└── .editorconfig # Editor configuration
Module Reference
lib/express.js — Factory & Exports
The createApplication() function:
- Creates a function
appthat delegates toapp.handle(req, res, next) - Mixes in
EventEmitter.prototypefor event support (e.g.,mountevent) - Mixes in the
application.jsprototype for all app methods - Creates
app.requestandapp.responseas prototype objects that will be set on incoming req/res - Calls
app.init()to configure defaults
Exported members:
| Export | Value |
|---|---|
express() (default) |
createApplication function |
express.application |
Application prototype |
express.request |
Request prototype |
express.response |
Response prototype |
express.Router |
Router constructor (from router package) |
express.Route |
Route constructor (from router package) |
express.json |
body-parser.json |
express.urlencoded |
body-parser.urlencoded |
express.raw |
body-parser.raw |
express.text |
body-parser.text |
express.static |
serve-static |
lib/application.js — Application Prototype
Key methods:
| Method | Visibility | Description |
|---|---|---|
init() |
Private | Initialize settings, lazy router getter, defaults |
defaultConfiguration() |
Private | Set defaults: x-powered-by, etag, env, query parser, subdomain offset, trust proxy, views, view, jsonp callback name; register mount event handler for inheritance |
handle(req, res, callback) |
Private | Request dispatch: sets headers, cross-references, prototypes, delegates to router.handle() |
use(fn) |
Public | Mount middleware or sub-application |
route(path) |
Public | Create a Route for the given path |
engine(ext, fn) |
Public | Register template engine |
param(name, fn) |
Public | Parameter pre-processing |
set(setting, [val]) |
Public | Get/set application setting (triggers compilation for etag, query parser, trust proxy) |
enabled(setting) / disabled(setting) |
Public | Boolean setting checks |
enable(setting) / disable(setting) |
Public | Set boolean settings |
all(path) |
Public | Register handler for all HTTP methods |
render(name, options, callback) |
Public | Render a view |
listen() |
Public | Create http.Server and listen |
path() |
Private | Compute absolute mount path |
HTTP method registration:
methods.forEach(function (method) {
app[method] = function (path) {
if (method === 'get' && arguments.length === 1) {
return this.set(path); // app.get(setting)
}
var route = this.route(path);
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
This creates app.get(), app.post(), app.put(), app.delete(), app.patch(), app.options(), app.head(), etc. — one for every HTTP method that Node.js supports. Note the special app.get() dual behavior: with one argument it reads a setting, with two or more it defines a route.
Router lazy initialization:
The router is created on first access via Object.defineProperty getter:
Object.defineProperty(this, 'router', {
configurable: true,
enumerable: true,
get: function getrouter() {
if (router === null) {
router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});
}
return router;
}
});
Note
The Router's
caseSensitiveandstrictoptions are set at construction time from the current application settings. Changingcase sensitive routingorstrict routingafter routes have been registered will not affect existing routes.
Sub-application mounting (app.use()):
When a mounted argument has both handle and set methods (i.e., it's an Express app), app.use():
- Sets
fn.mountpathto the mount path - Sets
fn.parentto the parent app - Wraps
fn.handle()in a middleware that saves and restoresreq/resprototypes - Emits
mounton the child app
lib/request.js — Request Prototype
Extends http.IncomingMessage.prototype via Object.create(). All properties/methods are added directly to the prototype object.
Getter properties (defined via defineGetter()):
| Property | Logic |
|---|---|
query |
Calls the compiled query parser fn on parseurl(this).query |
protocol |
Returns 'https' if socket is encrypted, else 'http'. When trust proxy is set, reads X-Forwarded-Proto |
secure |
this.protocol === 'https' |
ip |
proxyaddr(this, trustFn) |
ips |
All proxy addresses via proxyaddr.all(), reversed, socket address removed |
subdomains |
Host split by ., reversed, sliced by subdomain offset |
path |
parseurl(this).pathname |
host |
Host header; when trust proxy is set, reads X-Forwarded-Host |
hostname |
host with port stripped (IPv6-aware) |
fresh |
fresh(req.headers, { etag, last-modified }) for GET/HEAD with 2xx/304 |
stale |
!this.fresh |
xhr |
X-Requested-With header equals 'xmlhttprequest' (case-insensitive) |
Methods:
| Method | Description |
|---|---|
get(name) / header(name) |
Return header value. Special-cases Referrer/Referer to check both. Throws if name is missing or not a string. |
accepts(types) |
Content negotiation via accepts package |
acceptsEncodings(encodings) |
Encoding negotiation |
acceptsCharsets(...charsets) |
Charset negotiation |
acceptsLanguages(...languages) |
Language negotiation |
range(size, [options]) |
Parse Range header via range-parser |
is(types) |
Check Content-Type via type-is |
lib/response.js — Response Prototype
Extends http.ServerResponse.prototype. This is the largest core module (~2300 lines).
Key methods:
| Method | Key Behavior |
|---|---|
status(code) |
Validates: must be integer, 100–999. Throws TypeError or RangeError otherwise. |
send(body) |
String → sets Content-Type to HTML if not set. Object/Array → delegates to json(). Buffer → sets type to bin. Generates ETag if enabled. Strips body for 204/304. Sets Content-Length to 0 for 205. Checks req.fresh for 304. |
json(obj) |
Respects json escape, json replacer, json spaces settings. Sets Content-Type to application/json. |
jsonp(obj) |
Like json() but wraps in JSONP callback. Sanitizes callback name (strips non-[\w$.\[\]]). Rosetta Flash mitigation. Sets X-Content-Type-Options: nosniff. |
sendFile(path, opts, cb) |
Path must be absolute or opts.root must be set. Creates a send stream. Wires etag from app settings. |
download(path, name, opts, cb) |
Sets Content-Disposition via content-disposition. Resolves path if no root option. Delegates to sendFile(). |
redirect([status], url) |
Default status 302. Sets Location header. Sends HTML or text body via res.format(). Deprecation warnings for missing URL, non-string URL, non-number status. |
render(view, opts, cb) |
Merges res.locals into options as _locals. Defaults callback to self.send(str). Delegates to app.render(). |
cookie(name, val, opts) |
Object values get j: prefix + JSON. Signed values get s: + HMAC. maxAge (ms) → expires (Date) + maxAge (seconds). Default path: '/'. |
clearCookie(name, opts) |
Sets expires to new Date(1) (epoch + 1ms). Removes maxAge. |
format(obj) |
Content negotiation: matches Accept header against provided MIME types/extensions. Adds Vary: Accept. 406 error if no match and no default callback. |
lib/utils.js — Internal Utilities
| Export | Purpose |
|---|---|
methods |
Lowercased list of HTTP methods from http.METHODS |
etag / wetag |
Strong/weak ETag generator functions |
normalizeType(type) |
Convert extension to MIME type with quality params |
normalizeTypes(types) |
Batch version of normalizeType |
compileETag(val) |
Compile etag setting to function |
compileQueryParser(val) |
Compile query parser setting to function |
compileTrust(val) |
Compile trust proxy setting to function |
setCharset(type, charset) |
Set charset parameter in Content-Type string |
lib/view.js — View Class
The View constructor:
- Extracts file extension from the view name (or uses
defaultEngine) - Auto-requires the template engine by extension and looks for
__expressexport - Caches the engine in the shared
enginesobject - Resolves the full path via
lookup()→ checks<root>/<name>then<root>/<name>/index.<ext>
View.prototype.render() normalizes synchronous engine callbacks by deferring them via process.nextTick() to ensure consistent async behavior.
View.prototype.resolve() uses fs.statSync() to check if the resolved path is a file. This is a synchronous filesystem call on every uncached render.
Code Patterns and Conventions
Prototype-based architecture: Express does not use ES6 classes. All modules export plain objects that serve as prototypes, which are mixed into the app/req/res via Object.setPrototypeOf() or Object.create().
Chainable API: Most app.* and res.* methods return this for method chaining.
Dual-purpose methods:
app.get(): one arg = get setting, two+ args = define GET routeres.set()/res.header(): two args = set one header, one object arg = set multiple headers
Error creation: Uses http-errors package for HTTP errors (e.g., 406 in res.format()). Direct Error/TypeError/RangeError for validation errors.
Deprecation: Uses depd package for deprecation warnings (e.g., in res.redirect() for missing URL arguments).
Error Messages (Complete)
| Error Type | Message | Source | Trigger |
|---|---|---|---|
TypeError |
app.use() requires a middleware function |
lib/application.js:213 |
No function argument passed to app.use() |
Error |
callback function required |
lib/application.js:296 |
app.engine() called with non-function callback |
TypeError |
name argument is required to req.get |
lib/request.js:66 |
req.get() called without argument |
TypeError |
name must be a string to req.get |
lib/request.js:70 |
req.get() called with non-string |
TypeError |
Invalid status code: ${code}. Status code must be an integer. |
lib/response.js:67 |
Non-integer passed to res.status() |
RangeError |
Invalid status code: ${code}. Status code must be greater than 99 and less than 1000. |
lib/response.js:71 |
Status code outside 100–999 |
TypeError |
path argument is required to res.sendFile |
lib/response.js:379 |
res.sendFile() called without path |
TypeError |
path must be a string to res.sendFile |
lib/response.js:383 |
res.sendFile() called with non-string path |
TypeError |
path must be absolute or specify root to res.sendFile |
lib/response.js:393 |
Relative path without root option |
TypeError |
Content-Type cannot be set to an Array |
lib/response.js:674 |
res.set('Content-Type', [...]) called with array |
Error |
cookieParser("secret") required for signed cookies |
lib/response.js:748 |
Signed cookie without cookie-parser secret |
TypeError |
unknown value for etag function: ${val} |
lib/utils.js:148 |
Invalid etag setting value |
TypeError |
unknown value for query parser function: ${val} |
lib/utils.js:180 |
Invalid query parser setting value |
Error |
No default engine was specified and no extension was provided. |
lib/view.js:61 |
res.render('name') without view engine set and no file extension |
Error |
Module "${mod}" does not provide a view engine. |
lib/view.js:84 |
Template engine module lacks __express export |
Testing Strategy
Framework: Mocha with supertest for HTTP assertions, nyc for coverage.
Test scripts:
| Script | Command |
|---|---|
npm test |
mocha --require test/support/env --reporter spec --check-leaks test/ test/acceptance/ |
npm run test-cov |
Same tests with nyc HTML + text coverage report |
npm run test-ci |
Same tests with nyc lcov + text coverage (CI output) |
npm run test-tap |
Same tests with TAP reporter |
Test organization:
| Directory/Pattern | Coverage |
|---|---|
test/app.*.js |
Application methods: use, set, engine, param, render, listen, route, all, head, options, router, routes.error |
test/req.*.js |
Request properties: accepts, acceptsCharsets, acceptsEncodings, acceptsLanguages, baseUrl, fresh, get, host, hostname, ip, ips, is, path, protocol, query, range, route, secure, signedCookies, stale, subdomains, xhr |
test/res.*.js |
Response methods: append, attachment, clearCookie, cookie, download, format, get, json, jsonp, links, locals, location, redirect, render, send, sendFile, sendStatus, set, status, type, vary |
test/express.*.js |
Built-in middleware: json, raw, static, text, urlencoded |
test/config.js |
Application settings |
test/exports.js |
Module exports |
test/Route.js / test/Router.js |
Routing |
test/regression.js |
Regression tests |
test/middleware.basic.js |
Basic middleware behavior |
test/acceptance/*.js |
Integration tests for all 18 example applications |
Test environment: test/support/env.js sets NODE_ENV=test and NO_DEPRECATION=express to suppress deprecation warnings during tests.
Tip
Run
DEBUG=express:* npm testto see Express internals during test execution. The--check-leaksflag in the test command detects global variable leaks.
Potential Improvements
Based on the codebase and community issues:
-
View path containment (GitHub #7140):
View.prototype.lookup()does not enforce that resolved paths stay within theviewsdirectory, unlikesend(used byres.sendFile()). This is a potential path traversal risk if user input reachesres.render(). -
Synchronous view resolution:
View.prototype.resolve()usesfs.statSync(), blocking the event loop during template lookup on uncached renders. An async alternative would improve performance under high concurrency. -
BigInt JSON serialization (GitHub #7186, closed):
res.json()throws a crypticTypeErrorwhen the response body contains BigInt values. A more descriptive error message would improve developer experience. -
Double-slash mount paths (GitHub #4557):
app.use('//', handler)does not behave as expected. This is a known router-level issue. -
allowPrototypes: truein qs: The extended query parser passes{ allowPrototypes: true }toqs.parse(). While this improves compatibility, it can be a concern in security-sensitive contexts where prototype pollution matters.
Contributing
Linting:
npm run lint # check
npm run lint:fix # auto-fix
Running tests:
npm test # full test suite
npm run test-cov # with coverage report
Code style: ESLint configuration in .eslintrc.yml. EditorConfig in .editorconfig.
License: MIT
9. Integration Points
Template Engines
Express integrates with any template engine that exposes a __express(filePath, options, callback) function. The following engines work out of the box:
| Engine | Extension | Install |
|---|---|---|
| EJS | .ejs |
npm install ejs |
| Pug (Jade) | .pug |
npm install pug |
| Handlebars | .hbs |
npm install hbs |
| Mustache | .mustache |
npm install mustache-express |
For engines without __express, use app.engine():
app.engine('md', (filePath, options, callback) => {
const content = fs.readFileSync(filePath, 'utf8');
const html = marked.parse(content);
callback(null, html);
});
Session Stores
Express works with express-session and any compatible session store:
const session = require('express-session');
const RedisStore = require('connect-redis').default;
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'secret',
resave: false,
saveUninitialized: false
}));
Common Middleware Ecosystem
| Middleware | Purpose |
|---|---|
cors |
Cross-Origin Resource Sharing |
helmet |
Security headers |
compression |
Gzip/deflate response compression |
morgan |
HTTP request logger |
cookie-parser |
Cookie parsing |
express-session |
Server-side sessions |
passport |
Authentication strategies |
multer |
Multipart form data (file uploads) |
express-rate-limit |
Rate limiting |
method-override |
HTTP method override via header/query |
HTTP/HTTPS Server Integration
Express apps are standard Node.js request handlers:
const http = require('node:http');
const https = require('node:https');
const app = express();
// HTTP
http.createServer(app).listen(80);
// HTTPS
https.createServer({ key, cert }, app).listen(443);
// Both on same app
app.listen(port) is shorthand for http.createServer(app).listen(port). The returned http.Server instance supports all standard server events and methods.