Streams em Node.js: Leitura, Escrita e Transformação de Dados: Do Básico ao Avançado Já leu

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 . Este exemplo lê um arquivo em chunks de 64KB: Recebido chunk de ${chunk.length} bytes Controlando o Fluxo Você pode pausar e retomar

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.

Referências


Artigos relacionados