Entendendo Streams: A Base Conceitual
Streams são sequências contínuas de dados que fluem através de sua aplicação em pequenos pedaços, em vez de carregar tudo em memória. Imagine um tubo por onde a água flui gradualmente—é exatamente assim que streams funcionam em Node.js. Essa abordagem é essencial para aplicações que precisam processar grandes volumes de dados (arquivos gigantes, uploads, logs em tempo real) sem consumir memória excessiva.
O modelo de streams em Node.js segue o padrão de Readable (leitura), Writable (escrita), Duplex (ambos) e Transform (transformação). Cada tipo oferece um nível de controle diferente sobre o fluxo de dados. A principal vantagem é o backpressure—o sistema sabe quando pausar e retomar o envio de dados automaticamente, evitando gargalos.
Leitura de Dados com Streams Readable
Criando um Stream Readable
A forma mais comum de ler arquivos é usar fs.createReadStream(). Este exemplo lê um arquivo em chunks de 64KB:
const fs = require('fs');
const readable = fs.createReadStream('arquivo-grande.txt', {
encoding: 'utf8',
highWaterMark: 64 * 1024 // 64KB por chunk
});
readable.on('data', (chunk) => {
console.log(`Recebido chunk de ${chunk.length} bytes`);
// Processa cada chunk
});
readable.on('end', () => {
console.log('Arquivo lido completamente');
});
readable.on('error', (err) => {
console.error('Erro na leitura:', err);
});
Controlando o Fluxo
Você pode pausar e retomar streams manualmente. Isso é útil quando precisa controlar a taxa de consumo:
const readable = fs.createReadStream('dados.json');
readable.on('data', (chunk) => {
readable.pause();
setTimeout(() => {
console.log('Processando chunk...');
readable.resume();
}, 1000);
});
Escrita e Transformação de Dados
Streams Writable e Pipe
O método pipe() conecta um stream readable diretamente a um writable, gerenciando automaticamente o backpressure:
const fs = require('fs');
const readable = fs.createReadStream('origem.txt');
const writable = fs.createWriteStream('destino.txt');
readable.pipe(writable);
writable.on('finish', () => {
console.log('Arquivo copiado com sucesso');
});
Transform Streams para Processamento
Transform streams modificam dados enquanto fluem. Aqui convertemos texto para maiúsculas em tempo real:
const { Transform } = require('stream');
const fs = require('fs');
const maiusculasTransform = new Transform({
transform(chunk, encoding, callback) {
const maiorizado = chunk.toString().toUpperCase();
callback(null, maiorizado);
}
});
fs.createReadStream('entrada.txt')
.pipe(maiusculasTransform)
.pipe(fs.createWriteStream('saida.txt'));
Para casos mais complexos, como parsear JSON line-delimited:
const { Transform } = require('stream');
const fs = require('fs');
const parseJSON = new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
try {
const linha = chunk.toString().trim();
if (linha) {
const obj = JSON.parse(linha);
callback(null, obj);
} else {
callback();
}
} catch (err) {
callback(err);
}
}
});
fs.createReadStream('dados.jsonl')
.pipe(parseJSON)
.on('data', (obj) => {
console.log('Objeto processado:', obj);
});
Padrões Avançados e Boas Práticas
Composição de Streams
Combine múltiplos transforms para criar pipelines poderosos. Aqui filtramos e transformamos dados sequencialmente:
const { Transform } = require('stream');
const fs = require('fs');
const filtro = new Transform({
objectMode: true,
transform(obj, encoding, callback) {
if (obj.idade >= 18) {
callback(null, obj);
} else {
callback();
}
}
});
const formatador = new Transform({
objectMode: true,
transform(obj, encoding, callback) {
const formatado = `${obj.nome} (${obj.idade} anos)\n`;
callback(null, formatado);
}
});
fs.createReadStream('usuarios.jsonl')
.pipe(parseJSON)
.pipe(filtro)
.pipe(formatador)
.pipe(fs.createWriteStream('maiores.txt'));
Tratamento de Erros
Sempre implemente tratamento robusto de erros em pipelines:
const fs = require('fs');
const readable = fs.createReadStream('origem.txt');
const writable = fs.createWriteStream('destino.txt');
readable
.on('error', (err) => console.error('Erro na leitura:', err.message))
.pipe(writable)
.on('error', (err) => console.error('Erro na escrita:', err.message))
.on('finish', () => console.log('Pipeline concluído'));
Ou use a sintaxe moderna com pipeline():
const { pipeline } = require('stream');
const fs = require('fs');
pipeline(
fs.createReadStream('origem.txt'),
fs.createWriteStream('destino.txt'),
(err) => {
if (err) {
console.error('Erro no pipeline:', err.message);
} else {
console.log('Pipeline concluído com sucesso');
}
}
);
Conclusão
Streams são fundamentais em Node.js para trabalhar eficientemente com dados grandes. O trio Readable, Transform e Writable, combinado com pipe(), permite criar aplicações que processam informações de forma elegante e otimizada em memória.
Sempre prefira streams sobre carregar tudo em buffer para manipulação de arquivos, uploads/downloads e processamento de dados em tempo real. Use pipeline() para melhor tratamento de erros em versões modernas do Node.js.
Domine backpressure e composição de streams para construir arquiteturas robustas. Pequenas práticas—como definir highWaterMark apropriadamente e encadear transforms—fazem diferença significativa em produção.