Working with Streams in Node.js

Welcome to Day 18 of our Node.js Zero to 1 Series! 👱‍♂️👱‍♂️

Streams are a powerful feature in Node.js that enable handling of data that is read from or written to a source in a continuous, sequential manner. Streams are especially useful for working with large amounts of data or data that is produced or consumed over time, such as reading files, handling HTTP requests and responses, and processing data from various input/output sources.

Understanding Streams in Node.js

Streams in Node.js are instances of EventEmitter and can be one of four types:

  1. Readable Streams: For reading data.

  2. Writable Streams: For writing data.

  3. Duplex Streams: For both reading and writing data (e.g., a TCP socket).

  4. Transform Streams: A type of duplex stream where the output is computed based on the input (e.g., a gzip compression stream).

Streams are a fundamental concept in Node.js, providing an efficient way to handle large data sets.

Key Characteristics of Streams:

  • Event-driven: Streams are built on the EventEmitter class, emitting events such as data, end, error, and finish.

  • Buffering: Streams manage buffering internally, making them memory-efficient and suitable for handling large files or real-time data.

  • Pipelining: Streams can be pipelined together using the .pipe() method, allowing for easy composition of data processing steps.

Reading and Writing Data with Streams

Reading Data with Readable Streams

A readable stream can be created from various sources, such as files, HTTP responses, or other data sources. Here’s an example of reading a file using a readable stream:

Example: Reading a File

const fs = require('fs');

const readableStream = fs.createReadStream('example.txt', { encoding: 'utf8' });

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

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

readableStream.on('error', (err) => {
  console.error('Error reading file:', err);
});

In this example:

  • The fs.createReadStream method creates a readable stream from example.txt.

  • The data event is emitted when a chunk of data is available to read.

  • The end event is emitted when there is no more data to read.

  • The error event is emitted if an error occurs while reading the file.

Writing Data with Writable Streams

A writable stream can be used to write data to a destination, such as a file, HTTP response, or other writable destination. Here’s an example of writing to a file using a writable stream:

Example: Writing to a File

const fs = require('fs');

const writableStream = fs.createWriteStream('output.txt', { encoding: 'utf8' });

writableStream.write('Hello, World!\n');
writableStream.write('Writing data to a file using streams.\n');

writableStream.end('This is the end of the stream.');

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

writableStream.on('error', (err) => {
  console.error('Error writing to file:', err);
});

In this example:

  • The fs.createWriteStream method creates a writable stream for output.txt.

  • The write method writes data to the stream.

  • The end method signals the end of the stream and optionally writes a final chunk.

  • The finish event is emitted when all data has been written to the destination.

  • The error event is emitted if an error occurs while writing to the file.

Transform Streams for Data Processing

Transform streams are a type of duplex stream that can modify or transform the data as it is read and written. They are particularly useful for tasks such as data compression, encryption, or format conversion.

Example: Creating a Transform Stream

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

class UppercaseTransform extends Transform {
  _transform(chunk, encoding, callback) {
    const uppercased = chunk.toString().toUpperCase();
    this.push(uppercased);
    callback();
  }
}

const uppercaseTransform = new UppercaseTransform();

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

In this example:

  • The UppercaseTransform class extends the Transform class.

  • The _transform method is implemented to transform the input data (converting it to uppercase) and push the transformed data to the output.

  • The pipe method is used to connect the standard input stream (process.stdin) to the transform stream and then to the standard output stream (process.stdout).

Practical Examples with Streams

Example 1: Piping Data from One Stream to Another

Streams can be easily piped together to process data. Here’s an example of piping data from a readable stream to a writable stream:

const fs = require('fs');

const readableStream = fs.createReadStream('source.txt');
const writableStream = fs.createWriteStream('destination.txt');

readableStream.pipe(writableStream);

writableStream.on('finish', () => {
  console.log('Data has been copied from source.txt to destination.txt');
});

In this example, the pipe method is used to read data from source.txt and write it to destination.txt.

Example 2: Using Streams to Handle HTTP Requests and Responses

Streams are integral to handling HTTP requests and responses in Node.js. Here’s an example of creating a simple HTTP server that streams a file as the response:

const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  const readableStream = fs.createReadStream('example.txt');

  res.writeHead(200, { 'Content-Type': 'text/plain' });
  readableStream.pipe(res);

  readableStream.on('error', (err) => {
    res.writeHead(500);
    res.end('Error reading file');
  });
});

server.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

In this example:

  • An HTTP server is created using the http.createServer method.

  • When a request is received, a readable stream is created for example.txt.

  • The pipe method streams the file content as the HTTP response.

  • Error handling is added to respond with a 500 status code if an error occurs while reading the file.

Example 3: Compressing and Decompressing Data with Streams

Transform streams can be used to compress and decompress data. Here’s an example of using the zlib module to compress a file:

const fs = require('fs');
const zlib = require('zlib');

const readableStream = fs.createReadStream('example.txt');
const writableStream = fs.createWriteStream('example.txt.gz');
const gzip = zlib.createGzip();

readableStream.pipe(gzip).pipe(writableStream);

writableStream.on('finish', () => {
  console.log('File has been compressed to example.txt.gz');
});

In this example:

  • A readable stream is created for example.txt.

  • A writable stream is created for example.txt.gz.

  • The zlib.createGzip method creates a transform stream for gzip compression.

  • The pipe method is used to connect the readable stream to the gzip transform stream and then to the writable stream.

Conclusion

Streams in Node.js provide a powerful and efficient way to handle data, making it possible to process large amounts of data or real-time data with minimal memory overhead. By understanding and leveraging readable, writable, and transform streams, you can build scalable and high-performance applications.

In the next post, we will explore Building a RESTful API with GraphQL. Stay tuned for more insights!

10
Subscribe to my newsletter

Read articles from Anuj Kumar Upadhyay directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anuj Kumar Upadhyay
Anuj Kumar Upadhyay

I am a developer from India. I am passionate to contribute to the tech community through my writing. Currently i am in my Graduation in Computer Application.