Architecture
This page describes the high-level architecture of Express: the application factory, middleware stack, routing system, and request/response lifecycle. Understanding these components is essential for building applications with Express and for contributing to the framework itself.
System Overview
Express is a lightweight, unopinionated web framework for Node.js built around a middleware-based architecture. The core library resides in the lib directory and consists of six modules that collaborate to handle HTTP requests. The project root (index.js) re-exports the main factory function from lib/express.js.
The diagram shows the repository's major components. The core library (lib) is the foundation, with test providing unit and acceptance tests, and examples demonstrating usage patterns. The project root (index.js) serves as the entry point for consumers.
Runtime Topology
A single Node.js process loads the Express library. The application created from the core module listens on a local port for incoming HTTP requests. No external processes, message queues, or services are started by the repository itself. Communication happens solely through HTTP request/response between clients and the process.
Layering
- Foundation:
lib(core Express library) implements the application factory, routing, request/response extensions, view rendering, and utility functions. - Entry point: the project root re-exports the
createApplicationfunction fromlib/express. - Feature demonstrations:
examples,examples/mvc,examples/ejs, andexamples/route-separationshow usage patterns for the library (routing, MVC, view engines, and so on). - Testing harness:
testcontains unit tests for the core library;test/acceptancevalidates the example applications end to end.
Key Flows
- Application creation: the project root delegates to the
libfactory function. A developer calls it to create an Express application object. - HTTP request handling: an incoming HTTP request enters the application, passes through middleware defined via
librouting, is matched to a route, and a response is generated using helpers fromlib. - View rendering: when a response method such as
res.render()is called, the corelibview system locates and renders a template, then sends the output. - Content negotiation: core utilities in
libinspectAcceptheaders and select the best representation, as demonstrated by examples likeexamples/content-negotiation. - Error handling: errors thrown synchronously or asynchronously during request processing are caught by Express error-handling middleware, which formats and sends an error response.
Core Components
The Express library is composed of six modules in the lib directory, each with a specific responsibility:
| Module | File | Purpose |
|---|---|---|
| Application | lib/application.js | Defines the app object with methods like use, get, post, render, listen |
| Request | lib/request.js | Extends Node's http.IncomingMessage with Express-specific methods |
| Response | lib/response.js | Extends Node's http.ServerResponse with Express-specific methods |
| Router | lib/router/index.js | Implements the routing system for matching requests to handlers |
| View | lib/view.js | Handles template resolution and rendering |
| Utils | lib/utils.js | Provides helper functions used across the library |
The entry point lib/express.js ties these together by creating the application factory and re-exporting prototypes and middleware lib/express.js. The factory re-exports the application, request, and response prototypes along with the Route and Router constructors and the built-in body parser and static middleware (json, raw, static, text, urlencoded).
Utility Functions
The lib/utils.js module supplies the shared helpers that the other modules depend on. Notable exports include:
| Symbol | Purpose |
|---|---|
methods | The list of HTTP methods Express understands |
etag / wetag | Strong and weak ETag generation functions |
normalizeType / normalizeTypes | Normalize a single content type or an array of content types |
compileETag | Compile the etag setting into a function |
compileQueryParser | Compile the query parser setting into a function |
compileTrust | Compile the trust proxy setting into a function |
setCharset | Set the charset on a content type |
These helpers are what allow the application, request, and response modules to share consistent behavior for caching, content negotiation, and configuration parsing.
Application Factory
When a developer calls express(), the factory function in lib/express.js creates a new application object. This function:
- Creates an instance of the
appobject defined inlib/application.js - Initializes the internal middleware stack
- Sets up the default configuration (for example, view engine settings and trust proxy)
- Returns the app object ready for use
The application object is both a request handler and a factory for creating sub-applications. It can be passed directly to http.createServer() or mounted under another Express application. Because the app object is callable as (req, res, next), it can be used anywhere a middleware function is expected, which is what makes mounting sub-applications possible.
Middleware Stack
The middleware stack is the core mechanism by which Express processes HTTP requests. Every request passes through a series of functions (middleware) in the order they were registered.
How the Stack Works
- When the application starts, it initializes an empty middleware stack
- Each call to
app.use()orapp.METHOD()(for exampleapp.get(),app.post()) adds a layer to the stack - When a request arrives, Express iterates through the stack synchronously
- Each middleware function receives the request object, the response object, and a
nextfunction - A middleware can:
- End the request-response cycle by sending a response
- Pass control to the next middleware by calling
next() - Pass an error to the next error-handling middleware by calling
next(err)
Middleware Registration
The app.use() method registers middleware that runs for every request, regardless of path. The app.METHOD() methods register route-specific middleware that only runs when the request method and path match.
// From lib/application.js
app.use = function use(fn) {
// Adds a middleware layer to the stack
//...
};Error-Handling Middleware
Error-handling middleware is defined with four parameters: (err, req, res, next). When any middleware calls next(err), Express skips all regular middleware and looks for the next error-handling middleware in the stack. This is why the arity of the function matters: Express inspects the number of declared parameters to decide whether a layer is an error handler.
Routing System
The routing system matches incoming HTTP requests to registered handlers based on method and path. It is implemented by the Router constructor exported from lib/express.js and defined in lib/router/index.js.
Route Registration
Routes are registered using methods like app.get(), app.post(), app.put(), app.delete(), and so on. Each call creates a route with:
- A path pattern (string or regular expression)
- One or more handler functions
- Optional route-specific middleware
Route Matching
When a request arrives, Express:
- Extracts the HTTP method and URL path from the request
- Iterates through the middleware stack
- For each layer, checks whether the method and path match
- If matched, executes the layer's handler functions in sequence
- If no match is found, passes to the next layer
The routing system supports:
- Path parameters: segments prefixed with
:(for example/users/:id) are extracted and stored inreq.params - Regular expressions: paths can be defined as regex patterns
- Route arrays: multiple paths can be handled by a single route
- Route middleware: functions that run before the final handler
Route Separation
The examples/route-separation directory demonstrates organizing routes into separate modules. Each module (for example site.js, user.js, post.js) exports handler functions, and the main index.js imports them and defines routes using app.get() and similar methods. This pattern keeps route definitions in one place while the handler logic lives in resource-specific modules.
Request/Response Lifecycle
The complete lifecycle of an HTTP request through Express follows this flow:
Step 1: Request Arrival
An HTTP request arrives at the Node.js server. The app object, which is a valid request handler, receives the request.
Step 2: Middleware Processing
Express begins iterating through the middleware stack. Each middleware function receives (req, res, next). The request object (req) is an instance of Node's http.IncomingMessage extended with Express methods from lib/request.js. The response object (res) is an instance of Node's http.ServerResponse extended with methods from lib/response.js.
Step 3: Route Matching
When a middleware layer corresponds to a route, Express checks whether the request method and path match. If they do, the route's handler functions execute in sequence.
Step 4: Response Generation
The handler generates a response using methods such as:
res.send(): sends a response of various typesres.json(): sends a JSON responseres.render(): renders a view template and sends the outputres.redirect(): redirects the client to a different URL
Step 5: View Rendering (if applicable)
If res.render() is called, the View system (lib/view.js) locates the template file, compiles it using the configured template engine (for example EJS as shown in examples/ejs), and sends the rendered output.
Step 6: Response Sent
Once a response is sent, the request-response cycle ends. No further middleware executes.
Step 7: Error Handling
If any middleware calls next(err) or throws an error synchronously, Express skips the remaining regular middleware and looks for error-handling middleware. The error propagates through the stack until it is caught by an error handler or reaches the default error handler.
Configuration and Settings
Express applications support various configuration settings managed internally by the library. These include:
view engine: the default template engine extensionviews: the directory containing view templatestrust proxy: whether to trust proxy headers for IP addressesjsonp callback name: the query parameter name for JSONP callbacksetag: whether to generate ETags for responses
Settings are accessed and modified via the app.set() and app.get() methods. Several settings are compiled into functions by helpers in lib/utils.js: compileETag turns the etag setting into an ETag generator, compileQueryParser turns the query parser setting into a parser, and compileTrust turns the trust proxy setting into a predicate. Compiling once at configuration time avoids re-parsing the setting on every request.
Content Negotiation
Express provides built-in content negotiation through utilities in lib/request.js and lib/response.js. The req.accepts() method inspects the Accept header and returns the best matching content type. The res.format() method responds to requests based on the Accept header, selecting the appropriate representation. The normalizeType and normalizeTypes helpers in lib/utils.js back this behavior by normalizing content type strings before comparison.
Testing Architecture
The repository includes a comprehensive test suite in the test directory. Unit tests cover each core module independently, using a testing framework (likely Mocha) and supertest for HTTP assertions. Acceptance tests in test/acceptance validate the example applications end to end, making HTTP requests against the example apps and asserting on the responses. Keeping the acceptance tests separate from the unit tests means the examples double as executable documentation of the library's behavior.
Related Pages
- Overview
- Getting Started
- Architecture (this page)