the request lifecycle · decorate vs extend · mapped end to end

Express decorates Node.
Next extends the Web.

There are only two ways to give a request object more power, and each framework picked one. Express takes Node's IncomingMessage and glues new keys onto the very same instance — same object, more properties. Next.js App Router doesn't touch Node's object at all: NextRequest is a subclass of the Web platform's Request. This page traces a request through all three lifecycles, then opens up the machinery underneath.

&  decoration

Additive. IncomingMessage & { … } — every Node property survives, Express just layers more on. req instanceof IncomingMessage stays true.

extends  extension

A new foundation. class NextRequest extends Request — a different lineage, which is why the same handler runs in a bare V8 isolate at the Edge.

§ 1 · the foundation

Every request starts as the same two Node objects.

Before any framework, Node's HTTP server parses the raw bytes and hands you a readable request and a writable response. Frameworks decide what to do with these two.

clientraw HTTP bytes over TCP›
network + parserNode reads method, URL, headers; body becomes a stream›
IncomingMessagereadable request stream — req›
ServerResponsewritable response stream — res

What Node does not give you: req.params, a parsed req.query, an already-read req.body, res.json(), res.status(), routing, or middleware. That is the job frameworks take on — and the next section is the fork in how they take it.

§ 2 · the fork

Two camps, not three.

There are three route styles, but only two lineages for the object your handler receives. The whole question is: does the public request descend from Node's IncomingMessage, or is it a Web Request?

incoming HTTP request parsed by the runtime which lineage is the public req object? &  DECORATION → Node-shaped type Req = IncomingMessage & { … } Express Next Pages API same instance · base untouched · mutate then end req instanceof IncomingMessage → true extends → Web-shaped class NextRequest extends Request Next App Router a new value · Fetch base · build then return req instanceof IncomingMessage → false

Node-shaped (Express · Pages API)

Express and the older pages/api/* routes both receive Node's object, widened with extras. The body is a stream you (or middleware) drain; the response is a writable stream you mutate, then end().

Web-shaped (App Router)

App Router route.ts handlers receive a Web Request / NextRequest and return a Response. No Node object is involved — so the same handler runs on Node or at the Edge.

§ 3 · the lifecycles, end to end

Run a request down the stack.

Pick a framework, then press run to send a request through every stage — or step through it. Each stage is colored by which world owns it; click any stage to read what happens there.

stage 0 / 0
§ 4 · property provenance — request

Where does each req property come from?

Click any row. The same-named property arrives by a different mechanism in each world — and the badges cluster to prove the thesis: indigo = the untouched base, green = Express decoration, blue = Next's Web lineage.

native the base owns it inherited passed through + decorates Express adds it + extends Next's subclass adds it — do it by hand
property
IncomingMessage
Express req
NextRequest
§ 5 · the proof

Same base, layered. Vs. a different base, subclassed.

On the left, toggle Express's decoration on and off — the Node layer underneath stays exactly the same. On the right, the base itself is different; there is no Node object hiding below.

Express · decoration
req  =  IncomingMessage & { extras }
🔒 from IncomingMessage · never mutated
methodurl "/users/7?tab=info"headers
+ Express decoration layer
query getter→urlparams assignedbody middleware
req instanceof IncomingMessage → true
req.url after decoration → unchanged
The point: req.query is a getter that reads req.url and parses it. It never writes back. Decoration only ever adds.
Next · extension
class NextRequest extends Request
⬢ from Web Request · the Fetch standard
methodurl full absolute URLheaders Headers.json()
+ added by the subclass
nextUrl NextURLcookies RequestCookies
nextReq instanceof Request → true
nextReq instanceof IncomingMessage → false
The point: a whole different lineage — which is exactly why the same code runs in a bare V8 isolate at the Edge.
§ 6 · property provenance — response

Where does each res power come from?

The same inspector, the other direction. Notice that Next's response verbs are mostly static factories (they build a value) where Express's are instance methods (they mutate one).

native the base owns it inherited from the Web base + decorates Express adds it + extends Next's subclass adds it — do it by hand
capability
ServerResponse
Express res
NextResponse
§ 7 · the behavioural consequence

You poke a stream. You return a value.

This is what the type difference causes. A decorated ServerResponse is a writable stream, so you mutate it step by step. A NextResponse is-a Web Response — an immutable value — so you build it whole and hand it back. Step through both.

Node / Express
one ServerResponse · mutated in place
res ⟳ (writable stream)0 writes
Next.js
a Response value · built once, returned
nothing constructed yet
step 0 / 4
One honest seam: a Next response isn't totally frozen — its headers and cookies are mutable views you may set before returning. But the body and status are fixed at construction, and you never re-open a stream to write() again.
§ 8 · under the hood, at runtime

Decoration is a prototype swap. Extension is a superclass.

"Decorate vs extend" isn't only a metaphor — it's two literal JavaScript mechanisms. Express re-points the object's prototype at its own augmented one; Next's class … extends fixes the chain at construction via super(). Follow the [[Prototype]] links.

Express · prototype augmentation
lib/express.js + lib/request.js (simplified)
// 1. make an Express request proto that // inherits from Node's prototype: app.request = Object.create( http.IncomingMessage.prototype ); // 2. define getters on it Object.defineProperty(app.request, 'query', { get() { return parseQs(this.url); } }); // 3. per request, re-point the proto: Object.setPrototypeOf(req, app.request);
Object.getPrototypeOf(req) walks:
▼ __proto__
▼ __proto__
▼ __proto__
Inserted, not replaced. Express slips one link between the instance and Node's prototype, so req instanceof IncomingMessage stays true.
Next · class extends
set once by super(), at construction
// a normal ES class — the chain is fixed // the moment `new` runs. class NextRequest extends Request { constructor(input, init) { super(input, init); // becomes a real Request this.nextUrl = new NextURL(input); } } const req = new NextRequest(webRequest);
Object.getPrototypeOf(req) walks:
▼ __proto__
▼ __proto__
▼ __proto__
A separate lineage entirely. Nowhere is IncomingMessage, so req instanceof IncomingMessage is false — and nothing needs a Node runtime.
§ 9 · the same split, in the type system

How you'd add req.user yourself.

The runtime split has an exact mirror in TypeScript. Because Express decorates a shared interface, you extend it by re-opening it — declaration merging. Because NextRequest is a sealed class, there's no interface to merge into.

Express · declaration merging
re-open the global Express.Request interface
// types/express.d.ts declare global { namespace Express { interface Request { // re-opening it! user?: User; // merged in } } } // now typed everywhere — no cast: app.use((req, _res, next) => { req.user = lookup(req); next(); });
This is exactly how @types/express adds query and body. An open interface you keep widening — & by another name.
Next · no interface to merge
the class is sealed — wrap or carry context
// You CAN'T `declare` extra props onto a // class instance the same way. Options: // A) a typed wrapper / discriminated value type Authed = { req: NextRequest; user: User }; // B) carry it out-of-band (no req mutation) import { AsyncLocalStorage } from 'async_hooks'; const ctx = new AsyncLocalStorage<User>(); // ...later: ctx.getStore()
The sealed shape is the feature. You can't quietly bolt user onto the request, so request state stays predictable.
§ 10 · the two patterns, in the abstract

You've seen these before.

This isn't a Node-vs-Next quirk — it's two classic ways to add capability to an object, each making a different trade.

Decoration (&)

keep the instance · widen it · stay compatible
  • Identity is preserved. req instanceof IncomingMessage stays true, so every Node-based tool still works on it.
  • Additive & lazy. query is a getter computed from url; nothing is overwritten.
  • Composable. each middleware can assign more (body, user, cookies) as the request flows.
  • The cost: the object is mutable and untyped at the edges — req.body is unknown until some middleware ran.

Extension (extends)

new type · new base · new guarantees
  • A real subclass. NextRequest is-a Request — it inherits the whole Fetch contract for free.
  • Different foundation. built on the Web standard, not Node, so it isn't tied to a Node process.
  • Stable shape. nextUrl and cookies are always there with known types — no "did the middleware run?" doubt.
  • The cost: you can't reuse the Node/Express ecosystem; instanceof IncomingMessage is false.

The three styles, side by side.

Request object, then response model.

StylePublic requestUnderlying modelBody accessParams / query
ExpressreqNode IncomingMessage + extrasreq.body after middlewarereq.params, req.query
Next App RouterrequestWeb Request / NextRequestawait request.json()arg params, request.nextUrl.searchParams
Next Pages APIreqNode IncomingMessage + extrasreq.body via parserreq.query
StylePublic response modelHow you respond
ExpressNode ServerResponse + extrasres.status(200).json(data)
Next App RouterWeb Response / NextResponsereturn Response.json(data, { status: 200 })
Next Pages APINode ServerResponse + extrasres.status(200).json(data)
§ 11 · why Next moved toward Web APIs

One handler shape, many runtimes.

Coupling the public API to Node's IncomingMessage ties you to a Node process. Modeling it as a Web Request / Response gives App Router a single abstraction that travels.

Node runtime
adapts the platform request into a Web Request
Edge runtime
runs in a bare V8 isolate — no Node needed
maps to fetch()
the same Request/Response you already use client-side
Web Streams
streaming responses without Node-specific plumbing
Back to JIT-Atlas