RepoFold

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.

Rendering diagram…

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 createApplication function from lib/express.
  • Feature demonstrations: examples, examples/mvc, examples/ejs, and examples/route-separation show usage patterns for the library (routing, MVC, view engines, and so on).
  • Testing harness: test contains unit tests for the core library; test/acceptance validates the example applications end to end.

Key Flows

  • Application creation: the project root delegates to the lib factory 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 lib routing, is matched to a route, and a response is generated using helpers from lib.
  • View rendering: when a response method such as res.render() is called, the core lib view system locates and renders a template, then sends the output.
  • Content negotiation: core utilities in lib inspect Accept headers and select the best representation, as demonstrated by examples like examples/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:

ModuleFilePurpose
Applicationlib/application.jsDefines the app object with methods like use, get, post, render, listen
Requestlib/request.jsExtends Node's http.IncomingMessage with Express-specific methods
Responselib/response.jsExtends Node's http.ServerResponse with Express-specific methods
Routerlib/router/index.jsImplements the routing system for matching requests to handlers
Viewlib/view.jsHandles template resolution and rendering
Utilslib/utils.jsProvides 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:

SymbolPurpose
methodsThe list of HTTP methods Express understands
etag / wetagStrong and weak ETag generation functions
normalizeType / normalizeTypesNormalize a single content type or an array of content types
compileETagCompile the etag setting into a function
compileQueryParserCompile the query parser setting into a function
compileTrustCompile the trust proxy setting into a function
setCharsetSet 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:

  1. Creates an instance of the app object defined in lib/application.js
  2. Initializes the internal middleware stack
  3. Sets up the default configuration (for example, view engine settings and trust proxy)
  4. 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

  1. When the application starts, it initializes an empty middleware stack
  2. Each call to app.use() or app.METHOD() (for example app.get(), app.post()) adds a layer to the stack
  3. When a request arrives, Express iterates through the stack synchronously
  4. Each middleware function receives the request object, the response object, and a next function
  5. 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:

  1. Extracts the HTTP method and URL path from the request
  2. Iterates through the middleware stack
  3. For each layer, checks whether the method and path match
  4. If matched, executes the layer's handler functions in sequence
  5. 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 in req.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 types
  • res.json(): sends a JSON response
  • res.render(): renders a view template and sends the output
  • res.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 extension
  • views: the directory containing view templates
  • trust proxy: whether to trust proxy headers for IP addresses
  • jsonp callback name: the query parameter name for JSONP callbacks
  • etag: 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.

Generated from commit 53d4a0d