decorate · extend · the mechanisms underneath

Where req & res
get their powers.

Express takes Node's IncomingMessage and glues new keys onto the very same object — same instance, more properties. Next.js doesn't decorate anything; NextRequest extends Request — it subclasses an entirely different base: the Web platform's Fetch Request. The difference between the two is one character of TypeScript.

§ A · the operator that tells the whole story

& means decorate. extends means rebuild.

Read these three blocks top to bottom. Notice the relationship operator in each, and notice which base each one builds on.

1 · the Node basea Readable stream
// http.IncomingMessage — what the OS hands Node
interface NodeReq extends stream.Readable {
  method?: string;
  url?:    string;                // raw path: "/users/7?tab=info"
  headers: Record<string, string | string[] | undefined>;
}
2 · Express — DECORATION & intersection
// Same object. We only ADD to it.
type ExpressReq = NodeReq & {
  query:  Record<string, unknown>;   // GETTER, derived from .url
  params: Record<string, string>;    // ASSIGNED by the router
  body?:  unknown;                     // ASSIGNED by middleware
};
// req is STILL an IncomingMessage. url is STILL untouched.
3 · Next.js — EXTENSIONextends subclass
// A new class built on a DIFFERENT base: the Web standard.
class NextRequest extends Request {   // Request = Fetch API, NOT Node
  readonly nextUrl: NextURL;        // a parsed URL object
  readonly cookies: RequestCookies; // built-in cookie store
}
// nextReq is a Request. It is NOT an IncomingMessage.
&
Decoration is additive. Express widens the type of the object you already have. Every Node property survives, unchanged — Express simply layers more on top.
extends
Extension swaps the foundation. NextRequest is-a Web Request, not a decorated Node object. That's why it runs on the Edge, where there is no Node IncomingMessage to decorate in the first place.
Fig. 01 · property provenance

Where does each property come from?

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

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

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

On the left, toggle Express's decoration on and off — watch the Node layer underneath stay exactly the same. On the right, the base itself is different.

Express · decoration
req  =  IncomingMessage & { extras }
🔒 from IncomingMessage · never mutated
method url "/users/7?tab=info" headers
+ Express decoration layer
query getter→url params assigned body 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
method url full absolute URL headers Headers .json()
+ added by the subclass
nextUrl NextURL cookies RequestCookies
nextReq instanceof Request → true
nextReq instanceof IncomingMessage → false
The point: there is no Node object hiding underneath. A whole different lineage — which is exactly why the same code runs in a bare V8 isolate at the Edge.
§ C · the other half — the response

The same two operators, on the way out.

Everything above was req. The response is the mirror image: Express decorates Node's ServerResponse with conveniences, while NextResponse extends Response — subclassing the same Web base its request did.

1 · the Node basea Writable stream
// http.ServerResponse — you write bytes, then end()
interface NodeRes extends stream.Writable {
  statusCode: number;                         // a mutable field
  setHeader(k: string, v: string): void;
  end(chunk?: unknown): void;            // no .json(), no .send()
}
2 · Express — DECORATION & intersection
// Same writable stream. Just friendlier verbs bolted on.
type ExpressRes = NodeRes & {
  status(code: number): ExpressRes;       // chainable setter
  json(body: unknown): ExpressRes;       // stringify + set type + end
  cookie(n: string, v: string): ExpressRes;
};
// res is STILL a ServerResponse. Still mutated, then ended.
3 · Next.js — EXTENSIONextends subclass
// A subclass of the Web Response — an immutable value.
class NextResponse extends Response {
  readonly cookies: ResponseCookies;       // built-in cookie store
  static json(body, init?): NextResponse;  // factory → returns one
  static redirect(url): NextResponse;
  static next(): NextResponse;             // "continue" (middleware)
}
// You build one and RETURN it. You never poke a stream.
&
Express decorates the verbs. res.json(), res.status(), res.cookie() are sugar that ultimately call Node's setHeader / write / end. The lifecycle is identical to raw Node: mutate, then end.
extends
Next subclasses the Web Response. Its factories (.json, .redirect, .next) each return a finished response. The body and status are set at construction — there's no stream to write to.
Fig. 03 · response provenance

Where does each response power come from?

Same inspector, the other direction. Click a row. 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
Fig. 04 · 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 construct 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. You return the value; you don't drive it to completion.
§ D · under the hood, at runtime

Decoration is a prototype swap. Extension is a superclass.

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

Express · prototype augmentation
lib/express.js + lib/request.js (simplified)
// 1. Express makes its own request proto,
//    inheriting from Node's prototype:
app.request = Object.create(
  http.IncomingMessage.prototype
);
// 2. query/get()/etc. are defined 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. req instanceof IncomingMessage stays true because Node's prototype is still on the chain.
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 on this chain is IncomingMessage, so req instanceof IncomingMessage is false — and there's nothing Node-specific to need a Node runtime.
§ E · 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; you reach for a different tool entirely.

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 this is typed everywhere:
app.use((req, _res, next) => {
  req.user = lookup(req);   // ✓ no cast
  next();
});
This is exactly how @types/express adds query and body in the first place. The type system mirrors the runtime: an open interface you keep widening — & by another name.
Next · no interface to merge
the class is sealed — wrap or carry context instead
// 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 user
The sealed shape is the feature. You can't quietly bolt user onto the request, so request state stays predictable — at the cost of the casual extensibility Express gives you.
§ F · 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 makes a different trade.

Decoration  (A & B)

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 or destroyed.
  • 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.
Back to JIT-Atlas