π The 4 stream types
Type | What it does | Example in Node |
|---|---|---|
π Readable | You read from it | Incoming HTTP request body, |
βοΈ Writable | You write to it | HTTP response you send back, |
π Duplex | Both readable and writable | A web socket ( |
π Transform | Duplex, but modifies data as it passes through |
|
π Readable and βοΈ writable are the two you'll use constantly. Duplex and transform are more advanced/less common.
<div class="infographic-streams">
<style>
.infographic-streams {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--charge: #d64545;
--radius: 10px;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
color: var(--ink);
background: transparent;
}
.infographic-streams[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #13171F;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--charge: #e2685f;
}
.infographic-streams * { box-sizing: border-box; }
.infographic-streams .is-inner { padding: 0 clamp(1rem, 4vw, 1.5rem); }
.infographic-streams .is-eyebrow {
font-size: 0.72rem; font-weight: 600; letter-spacing: 0.09em;
text-transform: uppercase; color: var(--accent); margin: 0 0 0.4rem;
}
.infographic-streams h2 {
font-family: 'Newsreader', Georgia, serif; font-weight: 600;
font-size: clamp(1.5rem, 4vw, 2.05rem); line-height: 1.15;
margin: 0 0 0.45rem; letter-spacing: -0.01em;
}
.infographic-streams .is-sub {
font-size: 0.95rem; color: var(--ink-soft); margin: 0 0 1.6rem; max-width: 54ch;
}
.infographic-streams .is-grid {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.9rem; margin-bottom: 0.4rem;
}
@media (max-width: 560px) { .infographic-streams .is-grid { grid-template-columns: 1fr; } }
.infographic-streams .is-card {
text-align: left; border: 1px solid var(--line); border-radius: var(--radius);
background: var(--paper-alt); padding: 1.1rem 1.1rem 1rem; cursor: pointer;
font: inherit; color: inherit;
transition: border-color 0.15s ease, opacity 0.15s ease;
opacity: 1; position: relative;
}
.infographic-streams .is-card:hover { border-color: var(--accent); }
.infographic-streams .is-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.infographic-streams.is-active-set .is-card:not(.is-selected) { opacity: 0.5; }
.infographic-streams .is-card.is-selected { border-color: var(--accent); background: var(--accent-soft); }
.infographic-streams .is-card-top { display: flex; align-items: center; gap: 0.65rem; margin-bottom: 0.55rem; }
.infographic-streams .is-badge {
flex: none; width: 34px; height: 34px; border-radius: 9px; background: var(--paper);
border: 1px solid var(--line); display: flex; align-items: center; justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.10), 0 1px 1px rgba(0,0,0,0.06);
}
.infographic-streams[data-theme="dark"] .is-badge { box-shadow: 0 1px 4px rgba(0,0,0,0.45); }
.infographic-streams .is-badge svg { width: 18px; height: 18px; stroke: var(--accent); }
.infographic-streams .is-card-title { font-weight: 700; font-size: 1.02rem; }
.infographic-streams .is-card-desc { font-size: 0.87rem; color: var(--ink-soft); line-height: 1.42; margin: 0 0 0.7rem; }
.infographic-streams .is-dir { display: flex; align-items: center; gap: 0.35rem; height: 20px; margin-bottom: 0.6rem; }
.infographic-streams .is-dir svg { width: 16px; height: 16px; stroke: var(--ink-soft); flex: none; }
.infographic-streams .is-dir-box { flex: 1; height: 3px; border-radius: 2px; background: var(--line); }
.infographic-streams .is-card-eg {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 0.74rem; color: var(--accent); background: var(--paper);
border: 1px solid var(--line); border-radius: 6px; padding: 0.15rem 0.5rem; display: inline-block;
}
/* detail panel */
.infographic-streams .is-detail {
border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper);
padding: 1.1rem 1.2rem; margin-top: 0.9rem; display: none;
}
.infographic-streams .is-detail.is-shown { display: block; }
.infographic-streams .is-detail-head { display: flex; align-items: center; gap: 0.5rem; font-weight: 700; font-size: 0.95rem; margin-bottom: 0.6rem; }
.infographic-streams .is-detail-head svg { width: 16px; height: 16px; stroke: var(--accent); }
.infographic-streams .is-detail-text { font-size: 0.85rem; color: var(--ink-soft); line-height: 1.5; margin: 0.7rem 0 0; }
.infographic-streams .is-detail-text b { color: var(--ink); font-weight: 600; }
/* code stage */
.infographic-streams .is-stage { display: grid; grid-template-columns: 1fr; gap: 0.6rem; }
.infographic-streams .is-stage.is-two-col { grid-template-columns: 1fr 1fr; }
@media (max-width: 500px) { .infographic-streams .is-stage.is-two-col { grid-template-columns: 1fr; } }
.infographic-streams .is-codelabel {
font-size: 0.68rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
color: var(--ink-soft); margin: 0 0 0.35rem;
}
.infographic-streams .is-codebox {
background: var(--paper-alt); border: 1px solid var(--line); border-radius: 8px;
padding: 0.7rem 0.85rem; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 0.78rem; line-height: 1.6; min-height: 96px; color: var(--ink);
white-space: pre-wrap; word-break: break-word; position: relative;
}
.infographic-streams .is-codeline {
opacity: 0; transform: translateY(-4px); display: block;
transition: opacity 0.35s ease, transform 0.35s ease;
}
.infographic-streams .is-codeline.is-in { opacity: 1; transform: translateY(0); }
.infographic-streams .is-codeline .is-tick { color: var(--accent); margin-right: 0.4em; }
.infographic-streams .is-caret {
display: inline-block; width: 6px; height: 1em; background: var(--accent);
vertical-align: text-bottom; margin-left: 1px; animation: is-blink 0.9s step-end infinite;
}
@media (prefers-reduced-motion: reduce) { .infographic-streams .is-caret { animation: none; opacity: 0.6; } }
@keyframes is-blink { 50% { opacity: 0; } }
.infographic-streams .is-out-line { color: var(--accent); }
.infographic-streams .is-swap-icon { color: var(--ink-soft); }
.infographic-streams .is-tf-arrow { text-align: center; color: var(--ink-soft); font-size: 0.85rem; margin: 0.15rem 0; }
.infographic-streams .is-tf-arrow.is-pulse { color: var(--accent); }
.infographic-streams .is-hint { font-size: 0.76rem; color: var(--ink-soft); text-align: center; margin-top: 0.85rem; opacity: 0.85; }
</style>
<div class="is-inner">
<p class="is-eyebrow">Node.js Β· Streams</p>
<h2>The 4 stream types</h2>
<p class="is-sub">Every Node stream is one of these, based on which direction data can move through it.</p>
<div class="is-grid" id="isGrid">
<button class="is-card" data-key="readable" type="button" aria-pressed="false">
<div class="is-card-top">
<span class="is-badge"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg></span>
<span class="is-card-title">π Readable</span>
</div>
<p class="is-card-desc">You read from it.</p>
<div class="is-dir"><span class="is-dir-box"></span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></div>
<span class="is-card-eg">fs.createReadStream()</span>
</button>
<button class="is-card" data-key="writable" type="button" aria-pressed="false">
<div class="is-card-top">
<span class="is-badge"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg></span>
<span class="is-card-title">βοΈ Writable</span>
</div>
<p class="is-card-desc">You write to it.</p>
<div class="is-dir"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="m12 19-7-7 7-7"/></svg><span class="is-dir-box"></span></div>
<span class="is-card-eg">fs.createWriteStream()</span>
</button>
<button class="is-card" data-key="duplex" type="button" aria-pressed="false">
<div class="is-card-top">
<span class="is-badge"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m8 3-4 4 4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/></svg></span>
<span class="is-card-title">π Duplex</span>
</div>
<p class="is-card-desc">Both readable and writable.</p>
<div class="is-dir"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/></svg><span class="is-dir-box"></span><span class="is-dir-box"></span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 5 7 7-7 7"/></svg></div>
<span class="is-card-eg">net (socket)</span>
</button>
<button class="is-card" data-key="transform" type="button" aria-pressed="false">
<div class="is-card-top">
<span class="is-badge"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/></svg></span>
<span class="is-card-title">π Transform</span>
</div>
<p class="is-card-desc">Duplex, but modifies data in transit.</p>
<div class="is-dir"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg><span class="is-dir-box"></span></div>
<span class="is-card-eg">zlib (compression)</span>
</button>
</div>
<div class="is-detail" id="isDetail">
<div class="is-detail-head">
<svg id="isDetailIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></svg>
<span id="isDetailTitle"></span>
</div>
<div class="is-stage" id="isStage"></div>
<p class="is-detail-text" id="isDetailText"></p>
</div>
<p class="is-hint">Tap a type to watch it move data</p>
</div>
<script>
(function () {
var wrapper = document.currentScript.closest('.infographic-streams')
|| document.querySelector('.infographic-streams');
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') { dark = true; }
else if (bodyAttr === 'false') { dark = false; }
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
var reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var info = {
readable: {
icon: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
title: 'π Readable stream',
text: 'Data flows <b>out</b> of it, one chunk at a time β you attach a listener and consume it. An incoming HTTP request body is Readable.'
},
writable: {
icon: '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/>',
title: 'βοΈ Writable stream',
text: 'Data flows <b>in</b> β you call <b>.write()</b> and it handles buffering and backpressure for you. The HTTP response you send back is Writable.'
},
duplex: {
icon: '<path d="m8 3-4 4 4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/>',
title: 'π Duplex stream',
text: 'One object, two independent channels β reading and writing don\u2019t have to match up. A TCP socket (<b>net</b>) can send while still receiving.'
},
transform: {
icon: '<path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72"/><path d="m14 7 3 3"/><path d="M5 6v4"/><path d="M19 14v4"/><path d="M10 2v2"/><path d="M7 8H3"/><path d="M21 16h-4"/><path d="M11 3H9"/>',
title: 'π Transform stream',
text: 'A special Duplex where what comes out isn\u2019t what went in. <b>zlib</b> reads raw bytes on one side and writes compressed bytes on the other.'
}
};
var grid = wrapper.querySelector('#isGrid');
var detail = wrapper.querySelector('#isDetail');
var detailIcon = wrapper.querySelector('#isDetailIcon');
var detailTitle = wrapper.querySelector('#isDetailTitle');
var detailText = wrapper.querySelector('#isDetailText');
var stage = wrapper.querySelector('#isStage');
var cards = Array.prototype.slice.call(grid.querySelectorAll('.is-card'));
var timers = [];
function clearTimers() {
timers.forEach(function (t) { clearTimeout(t); clearInterval(t); });
timers = [];
}
function esc(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
// ---- Readable: lines arrive one by one, each "read", loop ----
function runReadable(box, lines, done) {
var i = 0;
function reset() {
box.innerHTML = '';
i = 0;
timers.push(setTimeout(step, 500));
}
function step() {
if (i >= lines.length) {
timers.push(setTimeout(reset, 1300));
if (done) done();
return;
}
var span = document.createElement('span');
span.className = 'is-codeline';
span.innerHTML = '<span class="is-tick">βΈ</span>' + esc(lines[i]);
box.appendChild(span);
requestAnimationFrame(function () { span.classList.add('is-in'); });
i++;
timers.push(setTimeout(step, 750));
}
if (reduced) {
box.innerHTML = lines.map(function (l) { return '<span class="is-codeline is-in"><span class="is-tick">βΈ</span>' + esc(l) + '</span>'; }).join('');
} else {
step();
}
}
// ---- Writable: text typed character by character, loop ----
function runWritable(box, text, done) {
var i = 0;
function reset() {
box.innerHTML = '<span class="is-codeline is-in"><span class="is-caret"></span></span>';
i = 0;
timers.push(setTimeout(step, 500));
}
function step() {
if (i > text.length) {
timers.push(setTimeout(reset, 1300));
if (done) done();
return;
}
box.innerHTML = '<span class="is-codeline is-in">' + esc(text.slice(0, i)) + '<span class="is-caret"></span></span>';
i++;
timers.push(setTimeout(step, 45));
}
if (reduced) {
box.innerHTML = '<span class="is-codeline is-in">' + esc(text) + '</span>';
} else {
step();
}
}
// ---- Transform: type input, pulse-morph into output, loop ----
function runTransform(box, arrowEl, input, output) {
function reset() { typeIn(); }
function typeIn() {
var i = 0;
box.innerHTML = '<span class="is-codeline is-in"><span class="is-caret"></span></span>';
arrowEl.classList.remove('is-pulse');
function step() {
if (i > input.length) {
timers.push(setTimeout(morph, 550));
return;
}
box.innerHTML = '<span class="is-codeline is-in">' + esc(input.slice(0, i)) + '<span class="is-caret"></span></span>';
i++;
timers.push(setTimeout(step, 55));
}
step();
}
function morph() {
arrowEl.classList.add('is-pulse');
timers.push(setTimeout(function () {
box.innerHTML = '<span class="is-codeline is-out-line">' + esc(output) + '</span>';
var el = box.querySelector('.is-codeline');
requestAnimationFrame(function () { el.classList.add('is-in'); });
timers.push(setTimeout(reset, 1500));
}, 450));
}
if (reduced) {
arrowEl.classList.add('is-pulse');
box.innerHTML = '<span class="is-codeline is-in is-out-line">' + esc(output) + '</span>';
} else {
typeIn();
}
}
function buildStage(key) {
stage.innerHTML = '';
stage.className = 'is-stage';
if (key === 'readable') {
stage.innerHTML =
'<div><p class="is-codelabel">incoming chunks</p><div class="is-codebox" id="isBoxA"></div></div>';
runReadable(stage.querySelector('#isBoxA'), ["chunk: 'Hello '", "chunk: 'World'", "chunk: '!'", "β stream ended"]);
}
if (key === 'writable') {
stage.innerHTML =
'<div><p class="is-codelabel">writing out</p><div class="is-codebox" id="isBoxA"></div></div>';
runWritable(stage.querySelector('#isBoxA'), "res.write('<h1>Hi there</h1>')");
}
if (key === 'duplex') {
stage.classList.add('is-two-col');
stage.innerHTML =
'<div><p class="is-codelabel">socket.on(\'data\')</p><div class="is-codebox" id="isBoxA"></div></div>' +
'<div><p class="is-codelabel">socket.write()</p><div class="is-codebox" id="isBoxB"></div></div>';
runReadable(stage.querySelector('#isBoxA'), ["β 'PING'", "β 'PING'", "β 'PING'"]);
runWritable(stage.querySelector('#isBoxB'), "socket.write('PONG')");
}
if (key === 'transform') {
stage.innerHTML =
'<div><p class="is-codelabel">in β out</p><div class="is-codebox" id="isBoxA"></div>' +
'<div class="is-tf-arrow" id="isArrow">β gzip.write() transforms it β</div></div>';
runTransform(stage.querySelector('#isBoxA'), stage.querySelector('#isArrow'), "'Hello World'", '1f 8b 08 00 f3 4a c1 5e ...');
}
}
function select(key, card) {
var already = card.classList.contains('is-selected');
cards.forEach(function (c) { c.classList.remove('is-selected'); c.setAttribute('aria-pressed', 'false'); });
wrapper.classList.remove('is-active-set');
clearTimers();
if (already) {
detail.classList.remove('is-shown');
stage.innerHTML = '';
return;
}
card.classList.add('is-selected');
card.setAttribute('aria-pressed', 'true');
wrapper.classList.add('is-active-set');
var d = info[key];
detailIcon.innerHTML = d.icon;
detailTitle.textContent = d.title;
detailText.innerHTML = d.text;
detail.classList.add('is-shown');
buildStage(key);
}
cards.forEach(function (card) {
card.addEventListener('click', function () { select(card.getAttribute('data-key'), card); });
});
})();
</script>
</div>π‘ Key fact: streams are EventEmitters
Every stream inherits from EventEmitter β the same class from the events lecture. So you already know the pattern: .on('eventName', callback).
π Readable Streams
β Key events: data (a chunk is ready), end (no more data)
β Key methods: pipe() (connect to another stream), read()
const fs = require('fs');
const readable = fs.createReadStream('bigfile.txt', {
encoding: 'utf8',
highWaterMark: 64 * 1024 // chunk size: 64kb
});
readable.on('data', (chunk) => {
console.log(`π¦ Received ${chunk.length} characters`);
});
readable.on('end', () => {
console.log('π No more data left to read');
});
readable.on('error', (err) => {
console.error('β Read error:', err.message);
});π₯οΈ Example output (for a ~150kb file):
π¦ Received 65536 characters
π¦ Received 65536 characters
π¦ Received 19339 characters
π No more data left to read(Number of data events depends on file size Γ· chunk size β a 150kb file with a 64kb chunk size produces 3 chunks, the last one smaller.)
βοΈ Writable Streams
β Key events: drain, finish (all data written)
β Key methods: write(), end()
const http = require('http');
const fs = require('fs');
http.createServer((req, res) => {
const readable = fs.createReadStream('video.mp4');
readable.on('error', (err) => {
res.statusCode = 500;
res.end('Server error');
});
readable.pipe(res);
}).listen(3000);π₯οΈ Example output (server-side console is silent unless you log; here's what the client sees when hitting the server):
$ curl <http://localhost:3000> --output video.mp4
% Total % Received % Xferd Average Speed Time
100 24.3M 100 24.3M 0 0 18.2M 0 0:00:01The file downloads in chunks rather than the server loading the whole 24MB video into memory first.
π Duplex Streams
Readable and writable at once β data flows both directions.
const net = require('net');
// server side
const server = net.createServer((socket) => {
socket.on('data', (data) => {
console.log('Server received:', data.toString());
});
socket.write('Hello client!');
}).listen(3000);
// client side
const socket = net.connect(3000, () => {
socket.write('Hello server!');
});
socket.on('data', (data) => {
console.log('Client received:', data.toString());
});π₯οΈ Example output:
Server received: Hello server!
Client received: Hello client!(Both logs come from the same process here since client + server run in one file β order between the two lines can vary slightly since they're async.)
π Transform Streams
A duplex stream that also changes the data as it passes through.
const fs = require('fs');
const zlib = require('zlib');
const readable = fs.createReadStream('bigfile.txt');
const writable = fs.createWriteStream('bigfile.txt.gz');
const transform = zlib.createGzip();
readable
.pipe(transform)
.pipe(writable)
.on('finish', () => console.log('β
Compression complete'))
.on('error', (err) => console.error('β Error:', err.message));π₯οΈ Example output:
β
Compression completeAnd in your file system, a new file appears: bigfile.txt.gz β noticeably smaller than bigfile.txt.
β οΈ Worth knowing
Everything above is about consuming streams Node already built for you (HTTP, fs, zlib, etc.). You can build custom streams too β that's a more advanced topic for later. For most apps, consuming existing streams is what matters day to day.
β Takeaway
π Streams process data in chunks β saves memory + time
π 4 types: π readable, βοΈ writable, π duplex, π transform β readable/writable are the ones you'll use most
π All streams are
EventEmitters, so.on('data', ...)/.on('end', ...)work exactly like beforeπ
pipe()is the go-to tool for wiring streams together with minimal code