"You met fetch in Track 5. The features that make it production-grade — undici tuning, ReadableStream interop, streaming uploads — are the part most devs never touch."
Streaming Uploads
Most fetch tutorials show body: JSON.stringify(...) for POSTs. That works for small payloads. For large ones — uploading a 4GB video, sending a streaming log — you want to stream the body without buffering it in memory:
import { createReadStream } from 'node:fs';
import { Readable } from 'node:stream';
const src = createReadStream('./huge.bin');
// Node accepts ReadableStream as a body — convert from Node Readable
const res = await fetch('https://uploader.example.com/upload', {
method: 'PUT',
body: Readable.toWeb(src),
duplex: 'half', // required when body is a stream
headers: { 'Content-Type': 'application/octet-stream' },
});
console.log('status:', res.status);
The duplex: 'half' option tells fetch that the body is streaming and the server won't read it after sending its response (which is the only model HTTP/1.1 supports). Without it, fetch throws because the spec requires explicit acknowledgement.
Server-Sent Events as a fetch Pattern
SSE is just an HTTP response with Content-Type: text/event-stream. fetch consumes it natively:
const res = await fetch('https://api.example.com/stream', {
headers: { Accept: 'text/event-stream' },
});
const reader = res.body
.pipeThrough(new TextDecoderStream())
.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
// value is a string like "data: {...}\n\n"
for (const line of value.split('\n')) {
if (line.startsWith('data: ')) {
const payload = JSON.parse(line.slice(6));
handleEvent(payload);
}
}
}
This is how cwkPippa's frontend consumes Claude's streaming responses. No SSE library, no third-party parser — just fetch + Web Streams + line splitting.
Tuning undici Directly
undici directly — same library underneath, lower-level API, more control:import { Pool } from 'undici';
const pool = new Pool('https://api.example.com', {
connections: 100, // max concurrent
pipelining: 10, // requests per connection
bodyTimeout: 30_000, // ms to wait for response body
});
const { body, statusCode } = await pool.request({
method: 'GET',
path: '/items',
});
for await (const chunk of body) {
// chunk is a Buffer
}You'd reach for this when fetch's overhead matters (10k+ req/sec services), or when you need pipelining, or when you want to swap the global agent for a proxy-aware one. For 99% of use cases, plain fetch is enough — but knowing undici is there means you can grow into it.The Response Object's Hidden Powers
Response is more than "a thing fetch returns." You can construct one to wrap streams as HTTP responses — useful for proxies and caches:
// Stream a response from one URL through transformation to a final response
const upstream = await fetch('https://api.example.com/big.json');
const upper = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk); // pass through, or transform here
},
});
const piped = upstream.body.pipeThrough(upper);
const response = new Response(piped, {
status: upstream.status,
headers: upstream.headers,
});
// `response` is now a fully-formed HTTP response object you could
// return from your own server handler, or .text(), .json() yourself