Node.js, part 16: Node 18 — global fetch, Web Streams, and the shrinking dependency list
Part 16from the Node.js series · 24 parts in all
Node 18 shipped in April 2022 with fetch as a global. Read casually that
sounds like convenience; in practice it ended a decade of one of the ecosystem's strangest
dependencies — every server carried an HTTP client it had to choose and justify. Part 16 is
what arrived and where fetch is still the wrong tool.
fetch, global, no import
const res = await fetch('https://api.example.com/items', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ ids: [1, 2, 3] }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const items = await res.json();
It is the WHATWG implementation, not a Node flavour, which is why it came with the rest of
the web platform in one release: fetch, Headers,
Request/Response, FormData, Blob, and
ReadableStream. Sharing an API with the browser is the actual prize — HTTP code
stops being runtime-specific, and that is worth more than the import line.
Web Streams: two families in one runtime
Now Node has both its own streams (part 3) and the web ReadableStream family.
They interop through Readable.fromWeb() and Readable.toWeb(), and you
should know which side you are on:
// The response body is a web stream; Node's pipeline wants a Node stream.
const { Readable } = require('stream');
const { pipeline } = require('stream/promises');
const fs = require('fs');
const res = await fetch(url);
await pipeline(Readable.fromWeb(res.body), fs.createWriteStream('download.bin'));
That pattern — fetch a large file and stream it straight to disk — is the one that matters,
because await res.arrayBuffer() puts the whole thing in memory.
Where fetch is the wrong tool
- Connection tuning.
fetchgives you no agent, no socket pool, no keep-alive settings. High-frequency internal calls to one host often still want theundicipackage directly, which is what powersfetchanyway. - Retries, timeouts, and cancellation are yours to build.
fetchdoes not time out on its own — a hung server hangs your request forever, so every call needssignal: AbortSignal.timeout(5000)(part 11). - Streaming request bodies and exotic protocols. Fine to fall back.
The dependency list for a typical service shrank by three or four packages in this release. Next: the test runner and watch mode that removed a few more.