Streams no Node.js: O Guia Completo
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 é:
- Use
pipelineem vez de.pipe()para tratamento de erros correto - Implemente backpressure para não sobrecarregar o consumidor
- Prefira
for await...ofcom 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.