req·····▶res  internals

00 An explorable comparison · Node · Express · Next

Two bloodlines
of req & res.

The same job — read a request, send a response — looks different in raw Node, in Express, and in Next.js. The reason is ancestry, and there are only two lines: one descends from Node streams, the other from the Web platform's Fetch standard. The twist is that Next belongs to both — its App Router crosses to the Web line while its Pages API stays Node-blooded, a sibling of Express. Everything else follows from where an object was born. Click the diagram to inspect it.

▤ The family tree
is‑a is‑a decorates (prototype) extends (subclass) Node.js core the stream module Web platform the Fetch standard IncomingMessage stream.Readable ServerResponse stream.Writable Request Fetch API Response Fetch API Express req + query · body Next Pages req NextApiRequest Express res + json · status Next Pages res NextApiResponse NextRequest + nextUrl · cookies NextResponse .json · .redirect
two questions the tree answers — kept separate
layer 1 runtime · who runs the code
layer 2 request shape · what your handler receives

Two independent axes. Bloodline answers layer 2 — and because Node streams can't exist without a Node process, it constrains layer 1 too.

◈ Click any box for an interactive deep dive · hover to highlight · Inspect to pan & zoom
Node line (streams) Express · decorates Node Next · App extends Web, Pages decorates Node

01 The one split

Express decorates Node. Next extends the Web.

There are only two ways to give a request object more power, and each picked one. Express takes Node's IncomingMessage and glues new keys onto the very same instance — same object, more properties. Next's App Router goes the other way: NextRequest is a subclass of the Web platform's Request — a different base entirely. Next still runs on Node by default; that's the runtime, not the request object, and the two come apart in the next section. The split itself is one character of TypeScript.

the difference in one line type ExpressReq = IncomingMessage & { … }  vs  class NextRequest extends Request { … }
// decoration — additive, same instance, base untouched
ExpressReq = IncomingMessage & { query, params, body }   → still an IncomingMessage

// extension — a new class on a different base
NextRequest extends Request                          → NOT an IncomingMessage

Because decoration only ever adds, every Node property survives unchanged. Because extension swaps the foundation, NextRequest can run on the Edge, where there is no Node IncomingMessage to decorate in the first place. But "Next" isn't one thing: its older Pages API routes decorate Node exactly like Express — so the real fault line runs between routers, not frameworks.

02 Two layers · and Next in both

Runtime is not request shape.

"But I thought Next used Node?" It does. The confusion comes from collapsing two questions that are actually independent. The runtime layer asks who executes your code — a full Node.js process, or a bare V8 isolate at the Edge. The request-API layer asks what object your handler is handed — Node's IncomingMessage, or the Web platform's Request. Express answers both with Node. Next's App Router answers the first with "Node or Edge" and the second with "Web."

And here is the twist the word bloodlines was hiding: Next's Pages API answers both with Node. In pages/api, req is an IncomingMessage and res a ServerResponse, decorated with res.json() and res.status() — the same decoration trick Express plays. So Next.js is not one bloodline. It has a child in each: the App Router on the Web line, the Pages API on the Node line, sitting right beside Express.

lock it in Running on Node does not require exposing Node's request object as your API. Express: here is Node's req/res, made nicer. Next App Router: here is a Web Request, return a Web Response — the runtime is ours to handle. Next Pages API: here is Node-style req/res, made Next-friendly.

Probe it three ways — flip the matrix to feel the two layers move independently, classify any object by its bloodline, or read the same task in every dialect at once.

▶ probe the bloodlines

03 The cast & their lineage

Ten objects, two bloodlines.

Knowing which base each object descends from explains every behavioural quirk that follows — what url contains, whether headers are a plain object or a Headers instance, whether you mutate or return. Grouped by bloodline the families separate cleanly — and Next turns up in each.

ObjectRoleLineageRelationship to its base

04 Request · the same job, four dialects

Reading what arrives.

Pick a task. The shorter the code gets, the more plumbing the layer is hiding for you — a raw string becomes an auto-parsed object becomes a typed helper. The Next column has two faces: flip it between the App Router (Web) and the Pages API (Node), or open the full four-column view.

05 Response · the same job, four dialects

Sending what you reply with.

The mirror image. Next's App Router verbs are mostly static factories that return a value; Express's are instance methods that mutate one — and the Pages API, true to its Node blood, mutates right alongside Express.

06 The behavioural consequence

You poke a stream. You return a value.

This is what the type difference causes — and notice the split tracks bloodline, not framework. A decorated ServerResponse is a writable stream, so you mutate it step by step; Express and the Node-blooded Next Pages API both live on this side. A NextResponse is-a Web Response — an immutable value — so the App Router constructs it whole and hands it back. Step through both.

▶ res lifecycle · mutate vs construct
Node · Express · Next Pages · mutate0 writes
Next App Router · construct & return
00 / 04
STEP 00

Allocate the response. One side will be poked five times; the other won't exist until it's whole.

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.

07 Provenance — everything on one card

Where each capability comes from.

Next splits into two columns here, and the resemblance is the whole point: the Pages API column lines up with Express, not with the App Router.

TraitNode coreExpressNext App RouterNext Pages API
FoundationNode streamsdecorated Node streamsWeb Fetch standarddecorated Node streams
Statefulnessmutablemutablereturn a valuemutable
URL parsingraw req.url stringreq.query / req.paramsreq.nextUrl objectreq.query object
Headersplain lowercased objectreq.get() helperWeb Headers (.get())plain lowercased object
Cookiesraw header stringcookie-parser neededbuilt‑in .cookiesbuilt‑in req.cookies
Bodydrain the stream by handexpress.json() middlewareawait req.json()req.body (auto)
Send JSONend(JSON.stringify())res.json()NextResponse.json()res.json()
RedirectwriteHead(302,…)res.redirect() · 302NextResponse.redirect() · 307res.redirect(307,…)
RuntimeNode onlyNode onlyNode + EdgeNode only
Primary useservers & frameworksclassic backend APIsroute handlers & middlewarelegacy API routes

08 Runtime & why it matters

The lineage decides where your code can run.

The two layers line up for everyone except the App Router. Raw Node, Express, and Next's Pages API all hand you Node streams, so they all need a real Node process. Only the App Router put on the Web platform's Request/Response, which is why its objects are immutable and the same handler can run in a bare V8 isolate at the Edge.

So the practical takeaway isn't "which API is nicer" — it's that decoration buys you the Node ecosystem, and extension buys you the Web platform and the Edge. Three of the four dialects chose decoration; one chose extension. You're picking a bloodline, and the ergonomics — and the runtimes you can deploy to — follow from it.

tl;dr Three of these are Node streams wearing different clothes — raw Node, Express, and Next's Pages API. One, the App Router, is a different species that happens to do the same job. That single split is the whole story.
Back to JIT-Atlas