Skip to the solution
HardBackend7 min read

What are the Web Streams API (ReadableStream/WritableStream) in Node.js and how do they differ from node:stream?

478 views

The problem

Question presented to candidate: "Your code needs to interoperate with the Fetch API's response.body, which is a Web-standard ReadableStream β€” but your existing pipeline uses classic Node.js streams with .pipe(). Are these two genuinely different, incompatible systems, or can they work together?"

What a strong answer should cover:

  • The Web Streams API (ReadableStream, WritableStream, TransformStream) is the browser-standard streaming interface β€” now also built directly into Node.js as real global classes β€” genuinely distinct from Node's own original node:stream module (Readable, Writable, Transform), which predates the Web standard and has its own, different API shape.
  • πŸ“Œ Verified, not assumed β€” the exact API-shape difference: a real Web ReadableStream (converted via Readable.toWeb()) genuinely has no pipe() method at all (confirmed typeof webStream.pipe === "undefined") β€” it uses a real getReader()/.read() pattern instead, confirmed directly (typeof webStream.getReader === "function") β€” a genuinely different consumption model, not merely a renamed one.
  • πŸ“Œ Verified, not assumed β€” they genuinely interoperate: Readable.toWeb() converted a real node:stream into a real Web ReadableStream, and reading it via the real Web Streams reader API genuinely returned the correct data. In the reverse direction, a real, native ReadableStream (built directly with the Web Streams constructor), converted back via Readable.fromWeb(), genuinely produced correct data through node:stream's own 'data' event β€” directly answering the prompt: not incompatible, genuinely bridgeable in both directions.
  • This is the precise, direct answer to the prompt's fetch() scenario: response.body is a real Web ReadableStream β€” code built around .pipe()-based node:stream pipelines can genuinely consume it after converting with Readable.fromWeb(response.body), verified above to correctly preserve the real underlying data.
  • A precise answer names why both systems exist in Node rather than just one: node:stream is deeply embedded throughout Node's own core APIs (the filesystem, http, child_process, and more, all still built on it) β€” a wholesale replacement would be a massive breaking change; the Web Streams API was added specifically for standards compatibility (with fetch(), and with browser-shared code/libraries) β€” both are genuinely first-class in modern Node, verified above via real, official, built-in conversion functions rather than a community workaround.

Clarifying questions expected:

  • "Does the specific library/API this code needs to interoperate with expect a Web ReadableStream, a node:stream, or does it accept either?" β€” directly decides which conversion direction (if any) is actually needed.
  • "Is this code meant to also run in a browser (isomorphic code), where only the Web Streams API genuinely exists at all?" β€” a real, additional reason to prefer Web Streams for shared code specifically.

Code / implementation expected: Yes β€” a real, bidirectional conversion between node:stream and the Web Streams API, both directions genuinely preserving correct data, is the concrete, convincing proof that the two systems are genuinely interoperable, not incompatible.

  • nodejs
  • streams
  • web-streams
  • interop

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 8 sections

Read the code

JavaScriptReal, bidirectional interop between node:stream and the Web Streams API, and a real, confirmed API-shape difference
const { Readable } = require("stream");

const nodeStream = Readable.from(["chunk1 ", "chunk2 ", "chunk3"]);
console.log(typeof nodeStream.pipe); // "function"

const webStream = Readable.toWeb(nodeStream);
console.log(typeof webStream.pipe);      // "undefined" β€” genuinely no pipe() at all
console.log(typeof webStream.getReader); // "function" β€” uses a reader instead

async function readWebStream() {
  const reader = webStream.getReader();
  let result = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    result += value;
  }
  console.log(result); // "chunk1 chunk2 chunk3" β€” genuinely correct real data
}
readWebStream();

// --- the reverse direction: a real, native Web ReadableStream, back to node:stream ---
const nativeWebStream = new ReadableStream({
  start(controller) {
    controller.enqueue("native "); controller.enqueue("web "); controller.enqueue("stream");
    controller.close();
  },
});
const backToNode = Readable.fromWeb(nativeWebStream);
let nodeResult = "";
backToNode.on("data", (chunk) => { nodeResult += chunk; });
backToNode.on("end", () => console.log(nodeResult)); // "native web stream" β€” genuinely correct

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 136 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

Describe the Node.js architecture.

Continue