expressjs/express
github.com/expressjs/express ↗ · 214 files · 700 KB
Fast, unopinionated, minimalist web framework for Node.js.
Overview
Express 5.2.1 is the minimalist Node.js HTTP framework, and this repository holds only its core: ~5,500 lines across six files in `lib/` that layer settings management, request/response sugar, and a template-resolution system on top of Node's `http` primitives. What is distinctive about the v5 line is how much has been pushed out of the tree — routing lives entirely in the external `router` package, body parsing in `body-parser`, static serving in `serve-static` — so this repo is mostly a prototype-mixin façade plus a large behavioural test suite (70 unit files, 18 acceptance files that boot the `examples/` apps). It targets Node >= 18 and ships to npm as a library; there is no build step, no bundler, and no storage layer. The audience is every Node web developer, and the maintenance posture is visibly conservative: pinned SHA GitHub Actions, OpenSSF Scorecard, CodeQL, dependabot with major-version updates ignored.
System map
A single npm entrypoint (`index.js` → `lib/express.js`) exposes a factory, `createApplication()`, that returns a callable function decorated with the application prototype and EventEmitter. Five runtime modules split responsibility: `application.js` (settings, dispatch, mounting, rendering), `request.js` and `response.js` (prototypes grafted onto `http.IncomingMessage`/`http.ServerResponse`), `view.js` (template lookup), and `utils.js` (compilers for etag/query/trust). The runtime boundary is deliberately thin — routing, body parsing and static file serving are re-exported from sibling npm packages rather than implemented here. There is no persistent storage; all state is per-app in-memory objects. Distribution is npm-only, driven by a GitHub release event.
- `index.js` is a three-line re-export of `lib/express.js`, and `package.json` restricts the published tarball to `LICENSE`, `Readme.md`, `index.js` and `lib/` — so `test/` and `examples/` never ship.
- `createApplication()` builds `app` as a closure that forwards to `app.handle(req, res, next)`, then uses `merge-descriptors` to mixin `EventEmitter.prototype` and the `application` proto (both with `redefine=false`), and finally calls `app.init()`.
- Per-app request/response prototypes are created with `Object.create(req, { app: {...value: app} })`, which is why every `req`/`res` getter can reach `this.app.get('trust proxy fn')` without a closure.
- Routing is not implemented in this repo: `lib/express.js` requires the external `router` package and re-exports `Router` and `Router.Route`; `app.router` is a lazy getter that instantiates `new Router({ caseSensitive, strict })` on first access.
- Body parsing is a pure re-export — `express.json`, `express.raw`, `express.text`, `express.urlencoded` all point straight at `body-parser`, and `express.static` at `serve-static`.
- All per-app state lives in three null-prototype objects created in `app.init()` — `this.cache` (compiled views), `this.engines` (extension→renderer), `this.settings` — plus `this.locals`; there is no database or persistence layer anywhere in `lib/`.
- `app.handle()` is the single dispatch boundary: it builds a `finalhandler` terminator, sets `X-Powered-By`, wires the `req.res`/`res.req` circular refs, swaps both prototypes with `Object.setPrototypeOf`, and delegates to `this.router.handle`.
- The external integration surface is 28 runtime dependencies, of which the load-bearing ones are `proxy-addr` (trust proxy), `qs`/`node:querystring` (query), `send`+`serve-static` (file IO), `etag`+`fresh` (conditional GET), `cookie`+`cookie-signature`, and `finalhandler`.
- There is no build step. The npm scripts are `lint` (eslint), `test` (mocha over `test/` and `test/acceptance/` with `--check-leaks`), `test-ci` (nyc → lcovonly), `test-cov` (nyc → html) and `test-tap`.
- CI runs a 2-OS × 9-Node matrix (ubuntu/windows × Node 18–26) in `ci.yml`, with a separate `legacy.yml` still exercising Node 16 and 17 despite `engines.node` declaring `>= 18`.
- Deployment is npm publish triggered by GitHub release `published`; the workflow derives the dist-tag from `package.json` version rather than the release branch (`next` for prereleases, `latest` for the newest major, `latest-<major>` for maintenance lines).
- Supply-chain hardening is explicit: `.npmrc` disables lockfiles, enforces a 7-day minimum release age, sets `ignore-scripts=true` and `allow-git=none`; every Action in CI is pinned to a full commit SHA.
Feature catalog
The user-facing surface is three prototypes plus a factory. `app` provides configuration (`set`/`get`/`enable`/`disable`/`enabled`/`disabled`), composition (`use`, `route`, `param`, `all`, one method per `http.METHODS` entry), templating (`engine`, `render`) and lifecycle (`listen`, `path`). `res` provides ~24 response helpers built around `res.send`. `req` provides content negotiation and a set of lazily-computed getters derived from headers and the trust-proxy setting. The `examples/` tree doubles as documentation for 26 usage patterns and as the fixture set for acceptance tests.
- HTTP verb methods are generated dynamically: `methods.forEach` iterates `require('node:http').METHODS` lowercased and defines `app[method]`, with `app.get(name)` special-cased to read a setting when called with exactly one argument.
- `app.all(path, ...fns)` registers the same handlers against every method in the generated `methods` list rather than using a wildcard.
- `app.set()` has four magic settings that compile a companion function: `etag`→`etag fn`, `query parser`→`query parser fn`, `trust proxy`→`trust proxy fn`; each compiler throws `TypeError` on unknown string values.
- Defaults set at boot: `x-powered-by` enabled, `etag` = `weak`, `query parser` = `simple` (node querystring), `subdomain offset` = 2, `trust proxy` = false, `jsonp callback name` = `callback`, `views` = `resolve('views')`, and `view cache` auto-enabled when `NODE_ENV === 'production'`.
- `app.use()` detects a mounted sub-app by duck-typing `fn.handle && fn.set`, wraps it in a `mounted_app` middleware that restores the parent's `req`/`res` prototypes on the way out, sets `mountpath`/`parent`, and emits `'mount'`.
- The `'mount'` listener installed in `defaultConfiguration` makes a child app prototype-inherit the parent's `request`, `response`, `engines` and `settings`, and drops its own default `trust proxy` so the parent's wins.
- `res.send` is the funnel for almost every response: it type-switches on the body, sets Content-Type, computes Content-Length (skipped when `Transfer-Encoding` is present), generates an ETag, downgrades to 304 when `req.fresh`, strips headers for 204/304/205, and skips the body for HEAD.
- `res.json` and `res.jsonp` share a `stringify()` helper honouring the `json replacer`, `json spaces` and `json escape` settings, where `json escape` rewrites `<`, `>`, `&` to `\u003c`/`\u003e`/`\u0026` to defeat HTML sniffing.
- `res.sendFile(path, opts, cb)` requires an absolute path or an explicit `opts.root`, `encodeURI`s the path, wires `opts.etag` from the app setting, and pipes a `send` stream through the local `sendfile()` state machine; `EISDIR` falls through to `next()`.
- `res.download()` layers `Content-Disposition` (via `content-disposition`) on top of `res.sendFile`, explicitly dropping any user-supplied `content-disposition` header and resolving the path when no `root` is given.
- `res.format(obj)` negotiates on the object's keys via `req.accepts`, always sets `Vary: Accept`, falls back to an optional `default` key, and otherwise calls `next(createError(406, { types }))`.
- `res.redirect` is itself implemented on top of `res.format`, emitting a plain-text or escaped-HTML body for the target, and uses `depd` deprecation warnings for missing/non-string URLs and non-numeric statuses.
Data flow
Data enters as a raw Node `IncomingMessage`, is re-prototyped in `app.handle` so that Express getters become available, then flows through the external router's middleware stack where `body-parser` populates `req.body` and the router populates `req.params`, `req.baseUrl` and `req.next`. Derived request values are computed lazily and are never cached — every read of `req.query`, `req.ip` or `req.host` re-runs its parser against the raw headers, gated by the compiled `trust proxy fn`. Output converges on `res.send`, which is where Content-Type, Content-Length, ETag and freshness negotiation are decided before `res.end`. Template rendering is a side channel: `res.render` → `app.render` → `View#render` produces a string that is then fed back into `res.send`. Anything that escapes the stack terminates in `finalhandler`.
- The input boundary is `app.handle(req, res, callback)`, which mutates the incoming Node objects in place via `Object.setPrototypeOf(req, this.request)` / `(res, this.response)` — Express never wraps or copies the request.
- `res.locals` is lazily initialised to `Object.create(null)` on each dispatch only if absent, so a sub-app mount reuses the parent's locals object rather than resetting it.
- `req.query` is recomputed on every property read: it fetches `query parser fn` from app settings, extracts the raw string with `parseurl`, and returns `Object.create(null)` when parsing is disabled (`query parser` set to `false`).
- The `extended` query parser routes through `qs.parse(str, { allowPrototypes: true })`, while the default `simple` parser is `node:querystring.parse` — two materially different object shapes behind one setting.
- All proxy-derived values funnel through one compiled predicate: `compileTrust` turns `true`, a hop count, a comma-separated string, an array or a function into `trust(addr, index)`, consumed identically by `req.protocol`, `req.ip`, `req.ips` and `req.host`.
- `req.protocol` and `req.host` both defensively split their `X-Forwarded-*` header on the first comma, taking only the left-most value.
- `req.ips` reverses `proxyaddr.all()` and pops the socket address, so the returned array is farthest-to-closest and excludes the direct peer.
- On the way out, `res.set('Content-Type', ...)` always runs the value through `mime.contentType()` to append a charset, and throws `TypeError` if an array is supplied for Content-Type.
- ETag generation is a two-step: `compileETag` picks `wetag`/`etag` from `lib/utils.js`, `res.send` calls it only when no ETag header is already present and a length was computed, and the buffer conversion is skipped for chunks under 1000 bytes when no ETag is needed.
- Render data is merged in a fixed precedence — `app.locals`, then `res.locals` (passed as `opts._locals`), then the caller's options — with `cache` defaulting to the `view cache` setting when not explicitly provided.
- Compiled views are memoised by name in `app.cache` only when `renderOptions.cache` is truthy, which is why development mode re-stats the filesystem on every render.
- `View#render` normalises synchronous engine callbacks by copying `arguments` and deferring through `process.nextTick`, guaranteeing the caller always sees an async callback.
Behavior traces
Four flows carry essentially all of Express's behaviour: application construction, request dispatch, response emission, and view rendering. Each is short and traceable by name — the framework is only three or four stack frames deep before control reaches an external package. The traces below name functions in call order as they appear in `lib/`.
- STARTUP: `require('express')` → `index.js` → `lib/express.js` → user calls `createApplication()` → `mixin(app, EventEmitter.prototype)` → `mixin(app, proto)` → `Object.create(req/res)` → `app.init()` → `app.defaultConfiguration()` → settings seeded and the `'mount'` listener attached → `Object.defineProperty(this, 'router', getter)` installs the lazy router.
- STARTUP (server): `app.listen(...)` → `http.createServer(this)` → if the last arg is a function it is wrapped with `once()` and registered on the server's `'error'` event → `server.listen.apply(server, args)`; the returned value is the `http.Server`, not the app.
- ROUTE REGISTRATION: `app.get('/x', fn)` → generated method sees `arguments.length > 1` → `this.route(path)` → `this.router.route(path)` (first access instantiates `new Router`) → `route.get(fn)`.
- DISPATCH: socket → `app(req, res)` closure → `app.handle` → `finalhandler(...)` built → `res.setHeader('X-Powered-By')` → `req.res`/`res.req` cross-links → both prototypes swapped → `res.locals` ensured → `this.router.handle(req, res, done)` hands off to the external router package.
- PRIMARY ACTION (`res.send('hi')`): typeof-string branch sets charset via `setCharset` or `this.type('html')` → `app.get('etag fn')` → length computed and `Content-Length` set unless `Transfer-Encoding` present → `etagFn(chunk, encoding)` → `this.set('ETag')` → `if (req.fresh) this.status(304)` → header stripping for 204/304/205 → `this.end(chunk, encoding)` (or bare `end()` for HEAD).
- JSON PATH: `res.json(obj)` → reads `json escape`/`json replacer`/`json spaces` → `stringify()` → sets `Content-Type: application/json` only if unset → delegates to `res.send(body)`, which then re-enters the string branch and applies the charset.
- JSONP PATH: `res.jsonp` → `stringify()` → reads the callback from `req.query[app.get('jsonp callback name')]` → takes `callback[0]` if an array → strips everything outside `[^\[\]\w$.]` → sets `X-Content-Type-Options: nosniff` and `text/javascript` → escapes U+2028/U+2029 → wraps in the `/**/ typeof cb === 'function' && cb(...)` Rosetta-Flash mitigation → `res.send`.
- RENDER PATH: `res.render(view, opts)` → `opts._locals = res.locals` → default callback `(err, str) => err ? req.next(err) : self.send(str)` → `app.render(name, opts, done)` → locals merged → cache consulted → `new View(name, { defaultEngine, root, engines })` → `view.lookup()` → `View#resolve` → `tryStat` → `tryRender(view, opts, done)` → `View#render` → engine → `process.nextTick` → `res.send(html)`.
- ENGINE RESOLUTION: inside the `View` constructor, if `opts.engines[this.ext]` is missing, Express computes `mod = this.ext.slice(1)` and performs a dynamic `require(mod).__express`, throwing `Module "x" does not provide a view engine.` when that export is not a function.
- VIEW LOOKUP fallback: `View#resolve` first tries `<dir>/<file>` and, failing that, `<dir>/<basename>/index<ext>` — both via synchronous `fs.statSync` wrapped in `tryStat`.
- FILE TRANSFER: `res.sendFile` → argument validation (throws on missing/non-string path, and on a relative path with no `root`) → `encodeURI(path)` → `opts.etag = app.enabled('etag')` → `send(req, pathname, opts)` → `sendfile(res, file, opts, cb)` → listeners registered → `file.pipe(res)`; the `onfinish` handler uses `setImmediate` to distinguish a completed stream from an aborted one.
- DOWNLOAD: `res.download` → argument shuffling for the (filename|options|callback) overloads → `Content-Disposition` built from `basename(name || path)` → caller headers copied except `content-disposition` → `opts = Object.create(opts)` so user options stay on the prototype → `resolve(path)` when no `root` → `this.sendFile(fullPath, opts, done)`.
Testing signals
Testing is mocha + supertest with `after` for multi-assertion coordination, and it is unusually thorough for a codebase this size: 70 unit files under `test/` plus 18 acceptance files under `test/acceptance/`, totalling ~15,400 lines against ~5,500 lines of `lib/`. Unit files are named one-per-API (`res.send.js`, `req.host.js`, `app.param.js`), which makes the mapping from source method to test file mechanical. Acceptance tests do not use fixtures — they `require('../../examples/<name>')` and drive the real example apps, so `examples/` is effectively production test surface. Coverage runs through nyc in CI across the whole matrix, is merged with lcov and shipped to Coveralls.
- The runner is mocha with `--require test/support/env --check-leaks`, covering both `test/` and `test/acceptance/`; `--check-leaks` means any accidental global assignment fails the suite.
- `test/support/env.js` forces `NODE_ENV=test` (which suppresses `logerror` output and keeps `view cache` disabled) and sets `NO_DEPRECATION=body-parser,express` to silence `depd` warnings.
- HTTP assertions run through `supertest` against the app function directly, without binding a port — e.g. `request(app).get('/').expect(200, done)`.
- `test/support/utils.js` provides the shared assertion vocabulary: `shouldHaveBody`, `shouldHaveHeader`, `shouldNotHaveBody`, `shouldNotHaveHeader`, `shouldSkipQuery` — with body comparison done on hex-encoded buffers.
- `test/support/tmpl.js` is a hand-rolled `$var.path` template engine used as the canonical view-engine double, avoiding a real engine dependency in most view tests.
- The largest test files track the riskiest code: `app.router.js` (1217 lines), `res.sendFile.js` (913), `express.urlencoded.js` (828), `express.static.js` (815), `express.json.js` (755), `Router.js` (636), `res.send.js` (635).
- Path-traversal behaviour is explicitly covered: `express.static.js` asserts handling of `/users/../todo.txt`, `/users/../../todo.txt` and the URL-encoded `/users/%2e%2e/%2e%2e/todo.txt`, and `res.sendFile.js` covers `fake/../name.txt` and `../name.txt` under a `root`.
- All 18 acceptance files import example apps rather than fixtures — `test/acceptance/auth.js` requires `../../examples/auth` and drives the full login/session/restricted flow including cookie extraction.
- Seven of the 26 examples have no acceptance test: `online`, `route-middleware`, `search`, `session`, `static-files`, `view-constructor`, `view-locals` — leaving the custom-`View`-constructor extension point exercised only by the example itself.
- `test/exports.js` deliberately mutates `express.application`, `express.request` and `express.response` prototypes to prove the extension contract holds — meaning test ordering has real cross-file coupling.
- `test/regression.js` is a deliberately tiny guard file — currently one case asserting that throwing after `res.end()` still yields a clean 200 response.
- Coverage is produced per matrix cell (`npm run test-ci` → nyc lcovonly), uploaded as `coverage-node-<ver>-<os>` artifacts, then merged in a dedicated `coverage` job with `lcov -a` and pushed to Coveralls.
Risk map
The security-sensitive surface concentrates in four places: proxy trust (which decides whether client-supplied `X-Forwarded-*` headers are believed), JSONP callback sanitisation, file transfer via `res.sendFile`/`res.download`, and dynamic `require()` inside the view system. Blast radius is dominated by `res.send` — `json`, `jsonp`, `sendStatus` and the default `render` callback all funnel through it, so any change there touches nearly every response. Coupling risks come from prototype mutation: `app.handle` re-prototypes live Node objects and mounting re-parents settings chains, so behaviour can change at a distance. Supply-chain posture is strong, but `.npmrc` disables lockfiles, so local installs are not reproducible.
- SECURITY — proxy trust: `req.ip`, `req.ips`, `req.protocol`, `req.secure` and `req.host` all believe client-supplied headers whenever `trust proxy fn` returns true; setting `trust proxy` to `true` makes `compileTrust` return a constant-true function, so `X-Forwarded-For`/`-Proto`/`-Host` become fully spoofable.
- SECURITY — dynamic require: the `View` constructor derives a module name from the view's file extension and calls `require(mod)`. An application that renders a view name built from user input can drive module resolution, and the thrown errors leak which module was attempted.
- SECURITY — JSONP: the callback is taken from `req.query` and filtered with `replace(/[^\[\]\w$.]/g, '')`; correctness of this allowlist plus the `/**/ typeof` wrapper and forced `X-Content-Type-Options: nosniff` are the only things standing between `res.jsonp` and reflected script injection.
- SECURITY — prototype keys: the `extended` query parser calls `qs.parse` with `allowPrototypes: true`, which is a deliberate compatibility choice that permits keys like `constructor` in parsed query objects.
- SECURITY — file IO outside repo root: `res.sendFile` only enforces "absolute path or `root` option" and `encodeURI`; every actual containment check is delegated to the `send` package, so a `send` regression is an Express path-traversal regression.
- SECURITY — information disclosure by default: `x-powered-by` is enabled in `defaultConfiguration` and `app.handle` sets the `X-Powered-By: Express` header on every response unless explicitly disabled.
- SECURITY — cookie signing depends on a foreign property: `res.cookie` reads `this.req.secret`, which Express never sets; it throws `cookieParser("secret") required for signed cookies` if `cookie-parser` was not installed upstream, making signing silently middleware-order dependent.
- BLAST RADIUS — `res.send` is the shared exit for `res.json`, `res.jsonp`, `res.sendStatus` and the default `res.render` callback; the HEAD-vs-body, 204/304/205 header stripping, ETag and Content-Length rules all live in that one function.
- FRAGILE COUPLING — mutation of live Node objects: `app.handle` calls `Object.setPrototypeOf` on the actual `IncomingMessage`/`ServerResponse`, and `mounted_app` must manually restore the parent prototypes after a sub-app completes; forgetting that restore silently leaks the child's prototype into sibling middleware.
- FRAGILE COUPLING — settings inheritance is by prototype chain (`Object.setPrototypeOf(this.settings, parent.settings)`), guarded by a string symbol `'@@symbol:trust_proxy_default'` that is deleted/redefined in two separate places to preserve back-compat.
- PERFORMANCE — synchronous file IO on the request path: `View#resolve` calls `fs.statSync` up to twice per lookup, and `view cache` is only auto-enabled when `NODE_ENV === 'production'`, so non-production deployments stat the filesystem on every render.
- PERFORMANCE — `req.query`, `req.host`, `req.protocol` and `req.ip` are non-memoised getters; each access re-parses. Middleware that reads `req.query` repeatedly pays the `qs`/`querystring` cost every time. (inferred)
Extension points
Express is designed to be extended by mutation rather than by configuration. Three exported prototypes (`express.application`, `express.request`, `express.response`) are public and explicitly documented as mutable by the test suite. Beyond that there are four first-class registries — the template-engine map, the `view` constructor, the compiled-function settings (`etag`/`query parser`/`trust proxy`), and route params — plus mounting, which composes whole apps. The verb table is itself generated from Node's `http.METHODS`, so new HTTP methods appear automatically.
- `app.engine(ext, fn)` is the template-engine registry: it normalises the leading dot and stores into `this.engines[extension]`, which `View` consults before falling back to a dynamic `require`.
- `app.set('view', CustomViewCtor)` swaps the entire view-resolution class — `app.render` reads `this.get('view')` and does `new View(name, { defaultEngine, root, engines })`, requiring only a `.path` property and a `.render(options, cb)` method.
- The three compiler settings all short-circuit on a function argument — `compileETag`, `compileQueryParser` and `compileTrust` each begin with `if (typeof val === 'function') return val`, so any of them can be fully replaced by user code.
- `express.application`, `express.request` and `express.response` are exported prototypes and the test suite asserts that mutating them affects all subsequently created apps — the documented monkey-patch seam.
- The idiomatic per-app variant of that seam is mutating `app.response` / `app.request` rather than the global prototype, as `examples/mvc` does by defining `app.response.message = function(msg){...}`.
- The HTTP verb table is not hardcoded — `exports.methods` maps `require('node:http').METHODS` to lowercase and `methods.forEach` generates both `app[method]` and the per-method registration used by `app.all`.
- `app.param(name, fn)` accepts either a single name or an array (recursing per element) and forwards to `this.router.param`, giving a named hook for pre-resolving route parameters.
- `app.use(path, subApp)` is the composition primitive: any object with `.handle` and `.set` is treated as a mountable app, receives `mountpath`/`parent`, and emits `'mount'` — which applications may listen to for their own inheritance logic.
- `express.Router` and `express.Route` are re-exported from the `router` package, so custom routers can be constructed independently and mounted with `app.use`.
- `res.format(obj)` is a per-response dispatch table keyed by MIME type or extension name, with an optional `default` key — the content-negotiation extension point, and itself the mechanism behind `res.redirect`'s body selection.
- `app.locals` and `res.locals` are the two data-injection points into rendering; `app.render` merges them in a fixed order with caller options, and `app.locals.settings` is pre-wired so templates can read app settings.
- Route organisation patterns are demonstrated as extension idioms rather than framework features: `examples/route-map` builds routes from a nested object literal, and `examples/mvc/lib/boot.js` auto-loads controllers into the app.
Contributor handoff
This is a mature, style-conservative library where the diff bar is high and behaviour is pinned by a very large test suite. Read `lib/express.js`, `lib/application.js`, then the method you are changing in `lib/response.js` or `lib/request.js`; the test file name mirrors the API name, so `res.send` changes belong in `test/res.send.js`. The single biggest trap is that `test/acceptance/*` boots the real apps in `examples/`, so editing an example breaks tests in a different directory. Dependencies are installed fresh (no lockfile, no committed `node_modules`), so `npm install` is a required first step.
- READ FIRST, in order: `lib/express.js` (the factory and the export surface), `lib/application.js` (settings, `handle`, `use`, `render`), then the specific prototype file you are touching — `lib/response.js` or `lib/request.js`.
- SETUP: `node_modules/` is absent and `.npmrc` sets `package-lock=false`, so you must run `npm install` before any test run; there is no build step to run afterwards.
- TESTS TO RUN: `npm test` for the full mocha suite (`test/` + `test/acceptance/`), `npm run test-cov` for an HTML coverage report, and `npm run lint` before committing — CI runs lint as a separate required job.
- LINT RULES that bite: 2-space `indent` with `MemberExpression: off`, `eqeqeq` with `allow-null`, `eol-last`, `no-trailing-spaces`, and a `no-restricted-globals` rule banning the global `Buffer` — you must write `const { Buffer } = require('node:buffer')`.
- STYLE: `lib/` is ES5-flavoured — `var` declarations, `'use strict'` at the top of every file, JSDoc blocks with explicit `@public`/`@private` on every exported member. Match it rather than modernising opportunistically.
- NODE BUILTINS are always imported with the `node:` prefix (`node:http`, `node:path`, `node:fs`, `node:net`, `node:querystring`, `node:buffer`, `node:events`) — follow this in new code and in tests.
- TEST NAMING is a strict mirror of the API: `test/res.<method>.js`, `test/req.<getter>.js`, `test/app.<method>.js`. A new `res.foo` needs a new `test/res.foo.js`, not an addition to a catch-all file.
- TRAP — cross-directory coupling: the 18 files in `test/acceptance/` `require('../../examples/<name>')`, so an edit to an example app fails tests that do not mention it. Check `test/acceptance/` before touching `examples/`.
- TRAP — `test/exports.js` permanently mutates `express.application`, `express.request` and `express.response` for the remainder of the mocha process; new global-prototype tests can interact with it.
- TRAP — `--check-leaks` is enabled, so any implicit global created during a test (including in `lib/`) fails the run with a leak error rather than an assertion error.
- TRAP — the test env sets `NO_DEPRECATION=body-parser,express`, so `depd` warnings you add will be invisible locally; assert on behaviour, not on console output.
- SAFE EDIT BOUNDARY — routing, body parsing and static serving are NOT in this repo. Bugs in path matching, `req.params`, `req.body` or directory traversal belong to `router`, `body-parser`, `send` and `serve-static`.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 141 | 100% |
| HTML | 8 | docs/config |
| YAML | 7 | docs/config |
| CSS | 4 | docs/config |
| Markdown | 4 | docs/config |
| JSON | 1 | docs/config |
Start reading here
Probable source entrypoint: index.js — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (6 files)
Source layout — lib/
| Path | Files |
|---|---|
lib/application.js | 1 |
lib/express.js | 1 |
lib/request.js | 1 |
lib/response.js | 1 |
lib/utils.js | 1 |
lib/view.js | 1 |
Repository structure
| Top-level path | Files |
|---|---|
test/ | 112 |
examples/ | 80 |
lib/ | 6 |
.github/ | 6 |
Docs found: Readme.md
Package
package.json — express v5.2.1, 44 declared dependencies.
acceptsafterbody-parserconnect-rediscontent-dispositioncontent-typecookiecookie-parsercookie-sessioncookie-signaturedebugdepd
CI & config entrypoints
.github/workflows/npm-publish.yml— GitHub Actions workflow.github/workflows/legacy.yml— GitHub Actions workflow.github/workflows/codeql.yml— GitHub Actions workflow.github/workflows/ci.yml— GitHub Actions workflow.github/workflows/scorecard.yml— GitHub Actions workflowpackage.json— npm scripts: lint, lint:fix, test, test-ci, test-cov, test-tap
Structural health signals
Average score 7.9/10 across 96 analyzed source files; 16 hotspots surfaced.
test/app.router.js— 1217 lines, score 4.5 (Large file, Deep nesting)test/express.urlencoded.js— 828 lines, score 5 (Large file, Deep nesting)test/res.send.js— 635 lines, score 5 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
9a34acf03cb82026-09-16 — fix(res.send): preserve ETag generation with Transfer-Encoding (#7459)3ce6d0eb86e92026-09-13 — ci: add npm staged publication with dist-tag support (#7464)53d4a0d606c02026-09-11 — docs: fix capitalization of GitHub Discussions in Readme (#7426)bed501c695a62026-09-08 — build(deps-dev): bump morgan from 1.11.0 to 1.12.0 (#7461)4e65ec983d332026-09-08 — build(deps): bump the github-actions group with 5 updates (#7462)