A Deep Dive into Streams in Node.js

By Guilherme Luiz Maia Pinto
Picture of the author
Published on
Node.js Streams Banner

Introduction

Node.js streams provide a way to handle large amounts of data efficiently. They enable applications to read or write data piece by piece, rather than loading it all into memory. This makes streams an essential feature for building scalable applications.

In this article, we'll explore the concept of streams, their types, and their use cases, with practical examples.


What are Streams?

Streams are objects that allow you to read data from a source or write data to a destination in a continuous manner. They are ideal for handling large files or incoming data like HTTP requests.

Streams come in four types:

  • Readable: Data can be read from the stream.
  • Writable: Data can be written to the stream.
  • Duplex: Both readable and writable.
  • Transform: A special duplex stream that modifies data as it is read or written.

Basic Example of a Readable Stream

const fs = require('fs');

const readableStream = fs.createReadStream('largeFile.txt', {
  encoding: 'utf8',
  highWaterMark: 16 * 1024 // 16 KB chunk size
});

readableStream.on('data', (chunk) => {
  console.log('New chunk received:', chunk);
});

readableStream.on('end', () => {
  console.log('No more data to read.');
});

Creating a Writable Stream

const fs = require('fs');

const writableStream = fs.createWriteStream('output.txt');

writableStream.write('Hello, world!\n');
writableStream.end('This is the end of the stream.');

writableStream.on('finish', () => {
  console.log('All data written to file.');
});

Piping Streams

Streams can be connected using the pipe method, allowing data to flow from a readable stream to a writable stream.

const fs = require('fs');

const readable = fs.createReadStream('input.txt');
const writable = fs.createWriteStream('output.txt');

readable.pipe(writable);

Transform Streams

Transform streams allow you to modify data as it passes through.

const { Transform } = require('stream');

const upperCaseTransform = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  }
});

process.stdin.pipe(upperCaseTransform).pipe(process.stdout);

Best Scenarios to Use Streams

  • File Operations: Reading/writing large files without loading them into memory.
  • Network Operations: Processing HTTP requests/responses in chunks.
  • Data Transformation: Modifying data in real time, such as encoding or compression.

Best Practices

Backpressure Management: Handle situations where the writable stream cannot process data as fast as the readable stream produces it.

const canWrite = writable.write(data);
if (!canWrite) {
  readable.pause();
  writable.on('drain', () => readable.resume());
}

Error Handling: Always handle error events on streams.

readable.on('error', (err) => console.error('Error:', err));
writable.on('error', (err) => console.error('Error:', err));

Use pipe When Possible: It simplifies code and manages backpressure automatically.


Conclusion

Streams are a cornerstone of Node.js, providing efficient ways to handle data. By mastering streams, you can build performant and memory-efficient applications. Whether you're dealing with files, HTTP requests, or real-time data processing, streams are an indispensable tool.

Stay Tuned

Want to become a Software Engineer pro?
The best articles and links related to web development are delivered once a week to your inbox.