Saltar para o conteúdo
Streams no Node.js: O Guia Completo
Node.js

Streams no Node.js: O Guia Completo

28 de maio de 2026·Paulo de Paula

Streams são um dos recursos mais poderosos do Node.js — e um dos mais mal compreendidos. Neste guia vou destrinchar cada tipo de stream com exemplos que você pode usar no dia a dia.

Por que streams existem?

Imagine processar um arquivo CSV de 2 GB. A abordagem ingênua carrega tudo na memória:

const fs = require('fs')

// Isso vai explodir com arquivos grandes
const data = fs.readFileSync('gigante.csv')
processData(data)

Streams resolvem isso processando os dados em pedaços (chunks), mantendo o uso de memória constante independente do tamanho do arquivo.

Os quatro tipos de Stream

1. Readable — leitura de dados

const { Readable } = require('stream')

const readable = new Readable({
  read(size) {
    this.push('chunk de dados')
    this.push(null) // sinaliza fim do stream
  }
})

readable.on('data', (chunk) => console.log(chunk.toString()))
readable.on('end', () => console.log('Finalizado'))

2. Writable — escrita de dados

const { Writable } = require('stream')

const writable = new Writable({
  write(chunk, encoding, callback) {
    console.log('Recebido:', chunk.toString())
    callback() // sinaliza que processou o chunk
  }
})

writable.write('primeiro chunk')
writable.write('segundo chunk')
writable.end()

3. Transform — transforma enquanto flui

O tipo mais útil para pipelines de processamento:

const { Transform } = require('stream')

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

process.stdin.pipe(maiusculas).pipe(process.stdout)

4. Duplex — leitura e escrita independentes

Usado em sockets e conexões de rede:

const { Duplex } = require('stream')

const duplex = new Duplex({
  read(size) {
    this.push('dado do lado read')
    this.push(null)
  },
  write(chunk, encoding, callback) {
    console.log('Lado write recebeu:', chunk.toString())
    callback()
  }
})

Pipeline: a forma certa de encadear streams

Nunca use .pipe() direto em produção — ele não propaga erros. Use pipeline do módulo stream:

const { pipeline } = require('stream/promises')
const fs = require('fs')
const zlib = require('zlib')

async function comprimirArquivo(entrada, saida) {
  await pipeline(
    fs.createReadStream(entrada),
    zlib.createGzip(),
    fs.createWriteStream(saida)
  )
  console.log('Arquivo comprimido com sucesso')
}

comprimirArquivo('dados.csv', 'dados.csv.gz')

Caso real: processar CSV grande linha por linha

const { pipeline } = require('stream/promises')
const fs = require('fs')
const readline = require('readline')

async function processarCSV(arquivo) {
  const fileStream = fs.createReadStream(arquivo)
  const rl = readline.createInterface({ input: fileStream })

  let linhaCount = 0

  for await (const linha of rl) {
    const campos = linha.split(',')
    // processa cada linha sem carregar tudo na memória
    await salvarNoBanco(campos)
    linhaCount++
  }

  console.log(`Processadas ${linhaCount} linhas`)
}

Backpressure: quando o produtor é mais rápido que o consumidor

const readable = gerarDadosRapido()
const writable = escreverNoBancoDevagar()

readable.on('data', (chunk) => {
  // Writable retorna false quando o buffer está cheio
  if (!writable.write(chunk)) {
    readable.pause() // pausa o produtor

    writable.once('drain', () => {
      readable.resume() // retoma quando o buffer esvazia
    })
  }
})

Conclusão

Streams são essenciais para trabalhar com dados grandes, arquivos, sockets e qualquer situação onde você não quer carregar tudo na memória. O segredo é:

  1. Use pipeline em vez de .pipe() para tratamento de erros correto
  2. Implemente backpressure para não sobrecarregar o consumidor
  3. Prefira for await...of com streams assíncronos quando possível

No próximo artigo vou explorar como usar streams com requisições HTTP para upload de arquivos sem consumir memória excessiva.