Overview
Relevant Files
index.jspackage.jsonlib/express.jslib/application.jsReadme.md
Express is a fast, unopinionated, minimalist web framework for Node.js that provides a robust foundation for building HTTP servers and APIs. It abstracts away the complexity of Node's native HTTP module while maintaining flexibility and performance.
Core Architecture
Express follows a modular, middleware-based architecture. The framework is initialized through a factory function that creates an application instance, which acts as both a request handler and an event emitter. The application instance inherits methods from the Express prototype and the Node.js EventEmitter, allowing it to handle HTTP requests and emit lifecycle events.
import express from 'express'
const app = express()
app.get('/', (req, res) => {
res.send('Hello World')
})
app.listen(3000)
Key Components
The framework consists of several core modules working together:
-
Application (
lib/application.js) - Manages configuration, settings, middleware stack, and route handling. Initializes default middleware and provides methods for routing and rendering. -
Router (
routerpackage) - Handles route matching and dispatching requests to appropriate handlers. Supports HTTP methods (GET, POST, PUT, DELETE, etc.) and middleware chaining. -
Request/Response (
lib/request.js,lib/response.js) - Extends Node's native request and response objects with convenience methods for headers, cookies, content negotiation, and more. -
View Engine (
lib/view.js) - Supports 14+ template engines for server-side rendering with configurable view directories and caching.
Middleware System
Express uses a middleware-based request processing pipeline. Middleware functions receive (req, res, next) and can modify requests, send responses, or pass control to the next middleware. This enables composable, reusable request handling logic.
app.use(express.json())
app.use(express.static('public'))
app.use((req, res, next) => {
console.log('Request received')
next()
})
Built-in Features
- Robust routing with support for parameters, wildcards, and regex patterns
- Content negotiation for handling multiple response formats
- HTTP helpers for redirects, caching, and status codes
- Cookie and session management through middleware
- Static file serving with
express.static() - Error handling with centralized error middleware
- View rendering with multiple template engine support
Dependencies
Express depends on specialized packages for core functionality: body-parser for request parsing, router for routing logic, send for file serving, finalhandler for error handling, and various utility packages for HTTP operations like accepts, content-type, and etag.
Architecture & Core Components
Relevant Files
lib/express.jslib/application.jslib/request.jslib/response.jslib/view.jslib/utils.js
Express is built on a modular architecture that extends Node.js's native HTTP server with a powerful middleware and routing system. The framework uses prototype-based inheritance to augment request and response objects with Express-specific functionality.
Core Application Factory
The entry point is lib/express.js, which exports a createApplication() function. This factory creates an Express app by:
- Creating a middleware function that acts as the main request handler
- Mixing in
EventEmitterprototype for event capabilities - Mixing in application methods from
lib/application.js - Creating custom request and response prototypes that inherit from Node.js's
IncomingMessageandServerResponse
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
app.request = Object.create(req, { app: { value: app } });
app.response = Object.create(res, { app: { value: app } });
app.init();
return app;
}
Application Lifecycle
The Application prototype (lib/application.js) manages the app's configuration and request handling:
- Initialization (
app.init()): Sets up caches, engines, settings, and lazily initializes the router - Request Handling (
app.handle()): Sets up circular references between req/res, applies Express prototypes, and delegates to the router - Middleware (
app.use()): Registers middleware functions or mounts sub-applications at specific paths - Routing (
app.route(), HTTP verbs): Creates isolated middleware stacks for specific paths and HTTP methods
Request & Response Extensions
Request (lib/request.js) extends http.IncomingMessage with methods for:
- Content negotiation:
accepts(),acceptsCharsets(),acceptsEncodings(),acceptsLanguages() - Header access:
get(),header() - Request properties:
ip,ips,hostname,protocol,secure,fresh,stale,xhr - Parsing:
is(),range()
Response (lib/response.js) extends http.ServerResponse with methods for:
- Status & headers:
status(),set(),get(),type() - Content:
send(),json(),jsonp(),render() - Redirects:
redirect(),location() - Cookies:
cookie(),clearCookie() - Streaming:
download(),sendFile()
Middleware & Router Integration
Express uses the router package for route matching and middleware execution. The router maintains a stack of middleware functions that are executed in order. When a request arrives:
- The app's
handle()method sets up prototypes and locals - The router processes middleware and routes in sequence
- Matched routes execute their handler functions
- Errors bubble up to error-handling middleware
View Rendering
The View class (lib/view.js) handles template rendering:
- Resolves template files from configured view directories
- Loads and caches template engines (EJS, Pug, Handlebars, etc.)
- Normalizes sync/async engine callbacks using
process.nextTick()
Configuration & Settings
The application maintains settings for:
- etag: ETag generation strategy (weak/strong)
- query parser: Query string parsing method
- trust proxy: Proxy trust configuration for IP detection
- view engine: Default template engine
- views: Directory path for template files
Settings are inherited by mounted sub-applications through prototype chains, enabling hierarchical configuration.
Loading diagram...
Routing System
Relevant Files
lib/application.jslib/express.jstest/Router.jstest/Route.jsexamples/route-middleware/index.js
Express uses a two-tier routing architecture: the Router handles request dispatching and middleware chaining, while Route objects manage HTTP method-specific handlers for individual paths. The Router is lazily instantiated on first access and configured with case sensitivity and strict routing options.
Router Architecture
The Router is the core request dispatcher. It maintains a stack of middleware and routes, matching incoming requests against registered paths and HTTP methods. When a request arrives, the Router iterates through its stack, executing matching middleware and routes in order. It supports nested routers through router.use(), enabling modular application structure.
Key Router methods:
router.use(path, handler)- Register middleware for all HTTP methodsrouter.get(),router.post(), etc. - Register handlers for specific HTTP verbsrouter.route(path)- Create or retrieve a Route object for a pathrouter.param(name, callback)- Register parameter handlersrouter.handle(req, res, done)- Dispatch a request through the middleware stack
Route Objects
A Route represents a specific path with isolated middleware stacks for each HTTP method. Routes are created via app.route(path) or router.route(path). Each Route maintains separate handler arrays for GET, POST, PUT, DELETE, and other methods, plus an .all() stack that runs for any HTTP method.
app.route('/users/:id')
.get(function(req, res) { /* handle GET */ })
.post(function(req, res) { /* handle POST */ })
.put(function(req, res) { /* handle PUT */ })
Middleware Chaining
Routes support multiple middleware handlers that execute sequentially. Each handler receives (req, res, next) and must call next() to proceed. Error handlers receive (err, req, res, next) and are invoked when errors occur.
app.get('/user/:id', loadUser, checkAuth, function(req, res) {
res.send('User: ' + req.user.name);
});
Parameter Handling
Route parameters (e.g., :id) are extracted and stored in req.params. Parameter callbacks registered via app.param(name, callback) execute before route handlers, enabling validation or data loading:
app.param('id', function(req, res, next, id) {
req.user = loadUserFromDB(id);
next();
});
Configuration Options
The Router accepts options during initialization:
caseSensitive- Treat/Usersand/usersas different routes (default: false)strict- Treat/usersand/users/as different routes (default: false)
These are controlled via app.set('case sensitive routing', true) and app.set('strict routing', true).
Error Handling
Errors thrown in handlers or passed to next(err) skip regular middleware and route to error handlers (4-parameter functions). Error handlers execute in order until one handles the error or passes it to the next handler.
app.use(function(err, req, res, next) {
res.status(500).send('Error: ' + err.message);
});
Nested Routers
Routers can be mounted at paths, creating modular route hierarchies:
var apiRouter = express.Router();
apiRouter.get('/users', function(req, res) { /* ... */ });
app.use('/api', apiRouter); // Routes available at /api/users
Middleware & Request Pipeline
Relevant Files
lib/application.jslib/express.jstest/middleware.basic.jstest/app.use.jsexamples/route-middleware/index.js
Request Pipeline Overview
When an HTTP request arrives, Express processes it through a carefully orchestrated pipeline. The application instance itself is a function that delegates to app.handle(), which sets up the request context and passes control to the router. The router then executes middleware and routes in registration order, with each middleware able to modify the request, response, or pass control to the next handler.
Loading diagram...
Middleware Registration with app.use()
The app.use() method registers middleware globally or for specific paths. It accepts flexible arguments: a path (optional), middleware functions, or arrays of middleware. Middleware registered without a path defaults to '/' and matches all requests.
// Global middleware
app.use(function(req, res, next) {
console.log('Request:', req.method, req.url);
next();
});
// Path-specific middleware
app.use('/api', function(req, res, next) {
res.setHeader('X-API', 'true');
next();
});
// Multiple middleware
app.use(fn1, fn2, fn3);
// Array of middleware
app.use([fn1, fn2], fn3);
Middleware Execution Flow
Each middleware function receives (req, res, next) and must call next() to proceed to the next handler. If next() is not called, the pipeline stops and the response must be sent. Middleware executes in registration order, allowing early middleware to modify the request before later handlers see it.
app.use(function auth(req, res, next) {
req.user = loadUser();
next();
});
app.use(function logging(req, res, next) {
console.log('User:', req.user.name);
next();
});
app.get('/', function(req, res) {
res.send('Hello ' + req.user.name);
});
Route-Level Middleware
Routes can have their own middleware stack that executes before the route handler. This enables per-route validation, authentication, or data loading without affecting other routes.
app.get('/user/:id', loadUser, checkAuth, function(req, res) {
res.send('User: ' + req.user.name);
});
In this example, loadUser and checkAuth execute before the final handler, each calling next() to proceed.
Error Handling Middleware
Error handlers are middleware with four parameters: (err, req, res, next). When an error is passed to next(err) or thrown in a handler, Express skips regular middleware and routes directly to error handlers.
app.use(function(req, res, next) {
next(new Error('Something went wrong'));
});
app.use(function(err, req, res, next) {
res.status(500).send('Error: ' + err.message);
});
Mounted Applications
Express apps can be mounted as middleware on other apps using app.use(path, app). The mounted app receives requests matching its mount path, with req.url adjusted to remove the mount prefix. This enables modular application structure.
var blog = express();
var api = express();
blog.get('/', function(req, res) {
res.send('Blog home');
});
api.get('/posts', function(req, res) {
res.json([]);
});
app.use('/blog', blog);
app.use('/api', api);
Request Context Setup
The app.handle() method prepares each request by setting up prototypes and locals. It establishes circular references between req and res, applies Express-specific prototypes to both objects, and initializes res.locals for template data. This ensures all middleware and routes have access to Express methods and properties.
app.handle = function handle(req, res, callback) {
req.res = res;
res.req = req;
Object.setPrototypeOf(req, this.request);
Object.setPrototypeOf(res, this.response);
if (!res.locals) {
res.locals = Object.create(null);
}
this.router.handle(req, res, done);
};
Finalhandler
If no middleware sends a response, the finalhandler package provides a default response. It logs errors in development mode and sends appropriate HTTP status codes (404 for no match, 500 for errors). This ensures every request receives a response, preventing hanging connections.
Request & Response Objects
Relevant Files
lib/request.jslib/response.jslib/utils.js
Express extends Node.js's native IncomingMessage and ServerResponse objects with convenience methods for handling HTTP requests and responses. These enhanced objects provide a developer-friendly API for content negotiation, header manipulation, cookie handling, and response formatting.
Request Object
The request object (req) extends Node's IncomingMessage with methods for inspecting incoming data and metadata.
Content Negotiation:
req.accepts('json', 'html') // Returns best match based on Accept header
req.acceptsEncodings('gzip', 'deflate')
req.acceptsCharsets('utf-8', 'iso-8859-1')
req.acceptsLanguages('en', 'es')
Request Properties:
req.protocol // 'http' or 'https' (respects X-Forwarded-Proto)
req.secure // Boolean shorthand for protocol === 'https'
req.ip // Client IP (respects trust proxy setting)
req.ips // Array of IPs from X-Forwarded-For
req.hostname // Host without port
req.host // Host with port
req.path // URL pathname
req.query // Parsed query string object
req.subdomains // Array of subdomains
Header & Type Inspection:
req.get('Content-Type') // Get header (case-insensitive)
req.is('json') // Check if request is JSON
req.fresh // Check if response is still fresh (ETag/Last-Modified)
req.stale // Inverse of fresh
req.xhr // Check if X-Requested-With: XMLHttpRequest
Range Requests:
req.range(1000) // Parse Range header for byte ranges (useful for video streaming)
Response Object
The response object (res) extends Node's ServerResponse with methods for sending data and managing headers.
Status & Headers:
res.status(200) // Set HTTP status code
res.set('Content-Type', 'json') // Set header (or pass object)
res.get('Content-Type') // Get header value
res.append('Set-Cookie', 'foo=bar') // Append to header
res.type('json') // Set Content-Type via MIME type
Sending Responses:
res.send('Hello') // Send string/buffer/object
res.json({ user: 'tj' }) // Send JSON with proper headers
res.jsonp({ user: 'tj' }) // Send JSONP with callback support
res.sendStatus(204) // Send status with standard message
res.sendFile('/path/to/file') // Stream file to client
res.download('/path/to/file') // Send file as attachment
Content Formatting:
res.format({
'text/plain': () => res.send('text'),
'text/html': () => res.send('<p>html</p>'),
'application/json': () => res.json({ message: 'json' })
})
Cookies & Redirects:
res.cookie('name', 'value', { maxAge: 900000, httpOnly: true })
res.clearCookie('name')
res.location('/foo/bar')
res.redirect(301, 'http://example.com')
Rendering & Links:
res.render('view', { title: 'Page' }) // Render template
res.links({ next: '/page/2', last: '/page/5' }) // Set Link header
res.vary('Accept') // Add to Vary header
res.attachment('filename.pdf') // Set Content-Disposition
Utility Functions
The lib/utils.js module provides helper functions for type normalization and ETag generation:
normalizeType('json') // Returns { value: 'application/json', params: {} }
normalizeTypes(['json', 'html']) // Normalize array of types
setCharset('text/html', 'utf-8') // Add charset to Content-Type
Method Chaining
Most response methods return this, enabling fluent API patterns:
res.status(200)
.set('Content-Type', 'application/json')
.json({ success: true })
Key Design Patterns
- Content Negotiation - Automatically select response format based on Accept headers
- Proxy Trust - Respect X-Forwarded-* headers when behind reverse proxies
- ETag Support - Automatic ETag generation for cache validation
- Streaming - Efficient file serving via
sendFile()anddownload() - Error Handling - Status code validation with clear error messages
View Rendering & Template Engines
Relevant Files
lib/view.jslib/application.jslib/response.jsexamples/ejs/index.jsexamples/markdown/index.js
Express provides a flexible view rendering system that decouples template engines from the core framework. Views are resolved from a configurable directory, rendered with data, and sent as HTTP responses.
View Resolution & Configuration
The View class in lib/view.js handles template file discovery and engine loading. When rendering a view, Express:
- Resolves the view name to a file path using the configured
viewsdirectory - Determines the template engine based on file extension or default engine setting
- Loads the engine module (cached for performance)
- Executes the engine with the template path and data
Key configuration methods:
app.set('views', './views'); // View directory (default: CWD/views)
app.set('view engine', 'ejs'); // Default engine extension
app.engine('html', require('ejs').__express); // Map extension to engine
Rendering Pipeline
When res.render(view, options, callback) is called:
- Merge locals: Combines
app.locals,res.locals, and render options - Cache check: Returns cached view if caching is enabled (production by default)
- View instantiation: Creates a
Viewobject that resolves the file path - Engine execution: Calls the template engine with path, merged options, and callback
- Response: Sends rendered HTML with 200 status and
text/htmlcontent type
// In route handler
res.render('users', { users: data }, function(err, html) {
if (err) return next(err);
res.send(html);
});
// Or without callback (auto-sends response)
res.render('users', { users: data });
Template Engine Integration
Template engines must export an __express function matching the signature: (path, options, callback). Express provides two integration patterns:
Built-in engines (EJS, Pug, etc.) export __express directly:
app.engine('ejs', require('ejs').__express);
Custom engines can be registered with any function:
app.engine('md', function(path, options, fn) {
fs.readFile(path, 'utf8', function(err, str) {
if (err) return fn(err);
var html = marked.parse(str);
fn(null, html);
});
});
View Locals & Data Flow
Data flows to templates through multiple layers:
Loading diagram...
Use res.locals in middleware to expose data to all subsequent routes:
app.use(function(req, res, next) {
res.locals.user = req.user;
res.locals.session = req.session;
next();
});
View Lookup & File Resolution
The View.prototype.lookup() method searches for templates in configured root directories. It supports two file patterns:
<name>.<ext>- Direct file match<name>/index.<ext>- Directory index pattern
This enables organizing views hierarchically while maintaining clean render calls.
Built-in Middleware & Utilities
Relevant Files
lib/express.jslib/utils.jstest/express.json.jstest/express.static.jstest/express.urlencoded.jstest/express.raw.jstest/express.text.js
Express provides built-in middleware and utility functions for common request handling tasks. These are exposed directly on the main express object and leverage external packages like body-parser and serve-static for robust, production-ready implementations.
Body Parsing Middleware
Express includes four body parsing middleware functions for handling different content types:
express.json(options) - Parses incoming request bodies with application/json content type. Populates req.body with the parsed JSON object.
express.urlencoded(options) - Parses URL-encoded form data (application/x-www-form-urlencoded). Supports both simple and extended syntax for nested objects and arrays.
express.text(options) - Parses plain text bodies (text/plain). Stores the text as a string in req.body.
express.raw(options) - Parses binary data (application/octet-stream). Stores the raw bytes as a Buffer in req.body.
All body parsers share common options:
limit- Maximum request body size (default:100kb). Accepts strings like'1mb'or byte numbers.type- Content-Type to parse. Can be a string, array, or function for custom matching.verify- Function(req, res, buf, encoding)called before parsing for custom validation or logging.inflate- Whether to handle compressed bodies (gzip, deflate). Default:true.strict- For JSON only; iftrue, only objects and arrays are parsed (default:true).extended- For URL-encoded only; iftrue, usesqslibrary for complex nested structures (default:true).
Static File Serving
express.static(root, options) - Serves static files from a directory. Requires a root path string.
Key options include:
maxAge- Cache-Control max-age in milliseconds or string like'1d'(default:0).cacheControl- Enable/disable Cache-Control header (default:true).lastModified- Include Last-Modified header (default:true).etag- Enable/disable ETag generation (default:true).acceptRanges- Enable HTTP range requests (default:true).dotfiles- How to handle dotfiles:'allow','deny', or'ignore'(default:'ignore').index- Index file(s) to serve for directories (default:['index.html']).redirect- Redirect to trailing slash for directories (default:true).fallthrough- Pass to next middleware on 404 (default:true).setHeaders- Function to customize response headers.
Utility Functions
The lib/utils.js module provides internal utilities for content negotiation and configuration:
normalizeType(type)- Converts shorthand types ('html') to MIME types ('text/html').normalizeTypes(types)- Batch version for arrays of types.compileETag(val)- Converts ETag configuration to a generator function.compileQueryParser(val)- Converts query parser config to a parsing function.compileTrust(val)- Converts proxy trust config to a validation function.setCharset(type, charset)- Adds charset parameter to Content-Type headers.
These utilities support flexible configuration: boolean values, strings, numbers, arrays, or custom functions.
Error Handling
Body parsers return 4xx errors for invalid input:
400- Malformed JSON, invalid encoding, or parse errors.413- Request entity too large (exceedslimit).415- Unsupported content type or encoding.
Errors include a type property (e.g., 'entity.parse.failed') and can be caught by error middleware.
Integration Example
const express = require('express');
const app = express();
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public', { maxAge: '1h' }));
app.post('/api/data', (req, res) => {
res.json(req.body);
});
app.listen(3000);
Common Patterns & Examples
Relevant Files
examples/hello-world/index.jsexamples/multi-router/index.jsexamples/mvc/index.jsexamples/error-pages/index.jsexamples/route-middleware/index.jsexamples/params/index.jsexamples/cookies/index.js
Express provides a rich collection of examples demonstrating core patterns and best practices. These examples serve as practical starting points for building web applications with different architectural approaches.
Getting Started: Hello World
The simplest Express application requires just a few lines of code:
var express = require('express');
var app = express();
app.get('/', function(req, res) {
res.send('Hello World');
});
app.listen(3000);
This pattern establishes the foundation: create an app instance, define routes, and start the server. The res.send() method automatically sets content-type headers and sends the response.
Organizing Routes with Multiple Routers
As applications grow, separating routes into logical modules becomes essential. The multi-router pattern uses app.use() to mount routers at specific paths:
app.use('/api/v1', require('./controllers/api_v1'));
app.use('/api/v2', require('./controllers/api_v2'));
Each controller module exports a router instance, enabling clean separation of concerns and versioning strategies.
Route-Level Middleware and Authorization
Middleware can be applied to individual routes to handle authentication, validation, and authorization:
app.get('/user/:id/edit', loadUser, andRestrictToSelf, function(req, res) {
res.send('Editing user ' + req.user.name);
});
Middleware functions execute in order, allowing you to load resources, check permissions, and pass data via req properties before reaching the final handler.
Parameter Handling and Validation
The app.param() method intercepts route parameters for preprocessing and validation:
app.param('user', function(req, res, next, id) {
req.user = users[id];
if (req.user) next();
else next(createError(404, 'User not found'));
});
This pattern centralizes parameter logic, reducing duplication across multiple routes.
Error Handling and Custom Error Pages
Express supports error-handling middleware with a 4-parameter signature (err, req, res, next):
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('500', { error: err });
});
Error handlers must be registered after all other middleware and routes. Content negotiation with res.format() allows serving different error representations (HTML, JSON, plain text).
Session Management and View Locals
The MVC example demonstrates session setup and exposing data to templates:
app.use(session({ secret: 'key', resave: false }));
app.use(function(req, res, next) {
res.locals.messages = req.session.messages || [];
next();
});
This pattern ensures view templates have access to session data without explicit passing.
Cookie Handling
Cookies are parsed and managed through middleware:
app.use(cookieParser('secret'));
app.post('/', function(req, res) {
res.cookie('remember', 1, { maxAge: 60000 });
res.redirect('/');
});
The cookieParser middleware populates req.cookies and req.signedCookies, while res.cookie() and res.clearCookie() manage cookie lifecycle.