# Data flow — expressjs/express

> Canonical page: https://codevetter.com/unpack/express/data-flow

# Data flow — expressjs/express

Scanned commit 9a34acf03cb818ff3f8bc40e44176e277a25cbb9 (2026-09-15). Generated by codevetter codevetter 1.12.0 on 2026-09-22.

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. — [lib/application.js#L152-L182](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L152-L182)
- `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. — [lib/application.js#L175-L178](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L175-L178)
- `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`). — [lib/request.js#L230-L241](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/request.js#L230-L241) [lib/utils.js#L162-L192](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/utils.js#L162-L192)
- 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. — [lib/utils.js#L162-L192](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/utils.js#L162-L192) [lib/utils.js#L267-L271](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/utils.js#L267-L271)
- 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`. — [lib/utils.js#L194-L215](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/utils.js#L194-L215) [lib/request.js#L297-L431](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/request.js#L297-L431)
- `req.protocol` and `req.host` both defensively split their `X-Forwarded-*` header on the first comma, taking only the left-most value. — [lib/request.js#L307-L314](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/request.js#L307-L314) [lib/request.js#L418-L431](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/request.js#L418-L431)
- `req.ips` reverses `proxyaddr.all()` and pops the socket address, so the returned array is farthest-to-closest and excludes the direct peer. — [lib/request.js#L357-L366](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/request.js#L357-L366) [test/req.ips.js](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/test/req.ips.js)
- 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. — [lib/response.js#L669-L691](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L669-L691) [test/res.set.js](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/test/res.set.js)
- 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. — [lib/response.js#L161-L193](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L161-L193) [lib/utils.js#L40-L51](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/utils.js#L40-L51) [lib/utils.js#L249-L258](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/utils.js#L249-L258)
- 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. — [lib/application.js#L522-L546](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L522-L546) [lib/response.js#L896-L920](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L896-L920)
- 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. — [lib/application.js#L536-L575](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L536-L575) [lib/view.js#L104-L128](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/view.js#L104-L128)
- `View#render` normalises synchronous engine callbacks by copying `arguments` and deferring through `process.nextTick`, guaranteeing the caller always sees an async callback. — [lib/view.js#L133-L162](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/view.js#L133-L162)
- Static file responses bypass `res.send` entirely — `sendfile()` attaches listeners for `directory`, `end`, `error`, `file`, `stream` plus `on-finished`, applies `options.headers` on the `headers` event, and then `file.pipe(res)`. — [lib/response.js#L923-L1010](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/response.js#L923-L1010)
- The terminal output boundary for unhandled requests and errors is `finalhandler(req, res, { env, onerror: logerror })`, constructed per-dispatch unless the caller supplied its own callback. — [lib/application.js#L153-L158](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L153-L158)
- Settings inheritance for mounted apps is prototype-chain based, not copy based: `Object.setPrototypeOf(this.settings, parent.settings)` means later parent mutations are visible to already-mounted children. (inferred) — [lib/application.js#L115-L122](https://github.com/expressjs/express/blob/9a34acf03cb818ff3f8bc40e44176e277a25cbb9/lib/application.js#L115-L122)

Every claim links to its cited files at the pinned commit. Citations identify source evidence; they do not establish runtime correctness. Independent CodeVetter scan — not reviewed by upstream maintainers.

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
