CSV Streaming no Node.js permite importar e exportar grandes planilhas sem carregar o arquivo inteiro na memória. O formato é simples para usuários, ERPs e ferramentas de análise, mas possui variações de delimitador, aspas, encoding, cabeçalhos e quebra de linha.
Uma implementação robusta usa parser especializado, backpressure, limites, validação por linha e lotes de banco. Dividir texto apenas por vírgulas falha quando um campo contém delimitador, aspas ou nova linha.
Exemplo de CSV
id,nome,email
1,Ana,ana@example.com
2,"Bruno, Silva",bruno@example.com
3,"Texto com
quebra de linha",carla@example.comO terceiro registro ocupa mais de uma linha física. Por isso, readline sozinho não é um parser CSV correto.
Importação com parser
import { createReadStream } from 'node:fs';
import { parse } from 'csv-parse';
import { pipeline } from 'node:stream/promises';
const parser = parse({
columns: true,
bom: true,
skip_empty_lines: true,
trim: true,
max_record_size: 256 * 1024
});
await pipeline(
createReadStream('users.csv'),
parser,
async function* (records) {
for await (const record of records) {
yield validateUser(record);
}
}
);Escolha uma biblioteca madura e fixe a versão. Opções e nomes variam entre pacotes.
Detectar delimitador?
Arquivos brasileiros frequentemente usam ponto e vírgula porque a vírgula aparece em números. Detecção automática pode errar. Prefira exigir configuração ou oferecer opções explícitas.
const allowedDelimiters = new Set([',', ';', '\t']);
if (!allowedDelimiters.has(delimiter)) {
throw new Error('Delimitador não permitido');
}Encoding
UTF-8 é a melhor convenção, mas sistemas legados podem exportar Windows-1252 ou ISO-8859-1. Detectar encoding automaticamente não é infalível. Permita uma lista pequena e converta em streaming.
Remova BOM quando necessário e registre o encoding escolhido.
Validação de cabeçalho
const expected = ['id', 'nome', 'email'];
function validateHeaders(headers) {
const normalized = headers.map(h => h.trim().toLowerCase());
for (const required of expected) {
if (!normalized.includes(required)) {
throw new Error(`Coluna obrigatória ausente: ${required}`);
}
}
}
Bloqueie cabeçalhos duplicados, pois um objeto JavaScript pode sobrescrever valores sem aviso.
Mapeamento de colunas
Não passe o registro diretamente ao banco. Converta cada campo.
function validateUser(row) {
const id = String(row.id ?? '').trim();
const name = String(row.nome ?? '').trim();
const email = String(row.email ?? '').trim().toLowerCase();
if (!id || id.length > 100) throw new Error('ID inválido');
if (!name || name.length > 200) throw new Error('Nome inválido');
if (!email.includes('@') || email.length > 320) {
throw new Error('E-mail inválido');
}
return { id, name, email };
}Números e datas
Não use Number(''), pois o resultado é zero. Diferencie vazio, ausente e valor inválido.
function parseInteger(value, field) {
const text = String(value ?? '').trim();
if (!/^-?\d+$/.test(text)) throw new Error(`${field} inválido`);
const result = Number(text);
if (!Number.isSafeInteger(result)) throw new Error(`${field} fora do limite`);
return result;
}Datas devem usar formato definido, como ISO 8601. Evite parsing dependente do locale do servidor.
Processamento em lotes
const batch = [];
for await (const row of parser) {
batch.push(validateUser(row));
if (batch.length === 500) {
await repository.upsertBatch(batch);
batch.length = 0;
}
}
if (batch.length) await repository.upsertBatch(batch);Escolha tamanho com benchmark. Lotes grandes aumentam memória e tempo de transação.
Staging table
Para importações críticas, carregue dados em tabela temporária ou de staging. Depois:
- valide constraints;
- gere relatório de erros;
- compare contagens;
- promova dados em transação controlada;
- arquive ou remova staging.
COPY
O comando PostgreSQL COPY é muito eficiente para grandes volumes. Ele exige cuidado com tipos, escaping e tratamento de erros. Uma estratégia comum é COPY para staging e SQL para validação e merge.
Política de erros
Defina antes do processamento:
- falhar no primeiro erro;
- aceitar linhas válidas e relatar inválidas;
- limitar quantidade de erros;
- processar tudo em staging e decidir depois.
Não deixe o comportamento implícito.
CSV Injection
Ao exportar para planilhas, células iniciadas com =, +, - ou @ podem ser interpretadas como fórmulas.
function safeSpreadsheetCell(value) {
const text = String(value ?? '');
return /^[=+\-@]/.test(text) ? `'${text}` : text;
}A proteção depende do consumidor e pode alterar o conteúdo. Para dados críticos, documente a transformação e ofereça formatos alternativos.
Exportação em streaming
import { stringify } from 'csv-stringify';
const csv = stringify({
header: true,
columns: ['id', 'nome', 'email'],
bom: true
});
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename="usuarios.csv"');
await pipeline(
Readable.from(repository.streamUsers(tenantId)),
async function* (users) {
for await (const user of users) {
yield {
id: safeSpreadsheetCell(user.id),
nome: safeSpreadsheetCell(user.name),
email: safeSpreadsheetCell(user.email)
};
}
},
csv,
res
);Backpressure
Streams de banco e resposta precisam cooperar. Não busque milhões de linhas com rows = await query(). Use cursor ou paginação por chave.
Consulte Streams e Backpressure no Node.js.
Compressão
CSV costuma comprimir muito bem. Para exportações grandes, ofereça .csv.gz e preserve streaming.
Upload seguro
Valide tamanho antes e durante leitura. Não confie apenas na extensão ou Content-Type. Armazene uploads temporários fora da pasta pública e remova arquivos parciais.
Veja Upload Seguro de Arquivos no Node.js.
Cancelamento
Se o cliente fechar a conexão, interrompa cursor, parser e gravação. Em jobs assíncronos, use estado de cancelamento e checkpoints.
Idempotência
Imports podem ser repetidos. Use chave natural, ID externo ou identificador de arquivo para evitar duplicações.
CREATE UNIQUE INDEX users_external_id_unique
ON users (tenant_id, external_id);Multi-tenancy
Associe o job ao tenant autenticado. Não aceite tenant ID dentro do CSV como autoridade. Todas as queries devem usar o contexto validado.
Observabilidade
Monitore bytes, linhas, válidas, inválidas, lotes, duração, memória, throughput e falhas. Registre número da linha e código do erro, sem expor dados pessoais.
Testes
- vírgula em campo com aspas;
- aspas escapadas;
- quebra de linha dentro da célula;
- CRLF e LF;
- BOM;
- ponto e vírgula;
- encoding alternativo;
- cabeçalho duplicado;
- registro gigante;
- fórmula maliciosa;
- cancelamento;
- reprocessamento.
Erros comuns
- usar
split(','); - carregar arquivo inteiro;
- não limitar registro;
- detectar delimiter sem confirmação;
- não validar cabeçalho;
- converter vazio em zero;
- ignorar CSV injection;
- uma transação gigante;
- não remover temporários;
- não definir política de erros.
Checklist
- parser confiável;
- delimitador explícito;
- encoding definido;
- limites de arquivo e registro;
- cabeçalhos validados;
- tipos convertidos;
- lotes ou staging;
- CSV injection tratada;
- backpressure;
- cancelamento e métricas.
Conclusão
CSV Streaming no Node.js permite integrar planilhas e sistemas legados sem esgotar memória. O segredo é tratar CSV como um formato complexo, não como texto separado por vírgulas.
Use parser especializado, limites, validação por linha e lotes. Proteja exportações contra fórmulas e defina claramente atomicidade e erros. Com streams e staging, arquivos grandes podem ser processados com previsibilidade.
Consulte o RFC 4180 sobre CSV e a documentação de Streams do Node.js.



