NDJSON no Node.js representa um objeto JSON por linha. O formato, também chamado de JSON Lines, permite processar grandes conjuntos de dados em streaming sem carregar um array completo na memória.
Cada linha é independente e termina com quebra de linha. Isso simplifica exportações, logs estruturados, pipelines, ingestão e comunicação entre processos. A aplicação pode validar, transformar e persistir um registro por vez, preservando backpressure.
Exemplo de arquivo
{"id":"1","name":"Ana"}
{"id":"2","name":"Bruno"}
{"id":"3","name":"Carla"}
Não existe vírgula entre linhas e o documento inteiro não é um array JSON válido. Cada linha isolada é JSON válido.
Gerando NDJSON
import { createWriteStream } from 'node:fs';
import { once } from 'node:events';
const output = createWriteStream('users.ndjson');
for await (const user of listUsers()) {
const line = JSON.stringify(user) + '\n';
if (!output.write(line)) {
await once(output, 'drain');
}
}
output.end();
await once(output, 'finish');Aguardar drain preserva backpressure e evita acumular dados na memória.
Lendo linha por linha
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const rl = createInterface({
input: createReadStream('users.ndjson'),
crlfDelay: Infinity
});
for await (const line of rl) {
if (!line.trim()) continue;
const item = JSON.parse(line);
await processItem(item);
}readline é simples, mas limites precisam ser aplicados. Uma única linha gigantesca ainda pode consumir muita memória.
Transform stream
import { Transform } from 'node:stream';
class NdjsonStringify extends Transform {
constructor() {
super({ writableObjectMode: true });
}
_transform(value, encoding, callback) {
try {
callback(null, JSON.stringify(value) + '\n');
} catch (error) {
callback(error);
}
}
}Use pipeline() para propagar falhas e fechar recursos.
await pipeline(
Readable.from(source),
new NdjsonStringify(),
createWriteStream('export.ndjson')
);Consulte Streams e Backpressure no Node.js.
Exportação HTTP
app.get('/exports/users.ndjson', async (req, res, next) => {
res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename="users.ndjson"');
try {
await pipeline(
Readable.from(repository.streamUsers(req.auth.tenantId)),
new NdjsonStringify(),
res
);
} catch (error) {
if (!res.headersSent) next(error);
}
});Depois que headers e parte do corpo são enviados, não é possível trocar para uma resposta JSON de erro normal. Registre a falha e encerre o stream.
Ingestão HTTP
app.post('/imports/users', async (req, res) => {
const result = await importNdjson(req, {
maxBytes: 100 * 1024 * 1024,
maxLineBytes: 256 * 1024,
maxItems: 1_000_000
});
res.status(202).json(result);
});Limite tamanho total, bytes por linha e quantidade de registros.
Parser incremental com limite
async function* parseNdjson(stream, maxLineBytes = 256 * 1024) {
let buffer = '';
for await (const chunk of stream) {
buffer += chunk.toString('utf8');
while (true) {
const index = buffer.indexOf('\n');
if (index === -1) break;
const line = buffer.slice(0, index).replace(/\r$/, '');
buffer = buffer.slice(index + 1);
if (Buffer.byteLength(line) > maxLineBytes) {
throw new Error('Linha excede o limite');
}
if (line.trim()) yield JSON.parse(line);
}
if (Buffer.byteLength(buffer) > maxLineBytes) {
throw new Error('Linha excede o limite');
}
}
if (buffer.trim()) yield JSON.parse(buffer);
}Para Unicode dividido entre chunks, use StringDecoder ou TextDecoder em modo streaming, evitando quebrar caracteres multibyte.
Validação por registro
for await (const raw of parseNdjson(req)) {
const user = userSchema.parse(raw);
await batch.add(user);
}O JSON pode ser sintaticamente válido e ainda violar o contrato. Consulte JSON Schema no Node.js.
Processamento em lotes
const batch = [];
for await (const item of parseNdjson(stream)) {
batch.push(validate(item));
if (batch.length === 500) {
await repository.insertBatch(batch);
batch.length = 0;
}
}
if (batch.length) await repository.insertBatch(batch);Lotes melhoram throughput sem exigir que todo o arquivo caiba na memória.
Atomicidade
Uma transação única para milhões de registros pode manter locks e WAL por muito tempo. Escolha uma política:
- tudo ou nada para arquivos pequenos;
- commit por lote com relatório de falhas;
- staging table e validação antes da promoção;
- job assíncrono com checkpoint.
Relatório de erros
{
"line": 418,
"code": "INVALID_EMAIL",
"message": "E-mail inválido"
}Não devolva o registro completo se ele contém dados pessoais. Limite a quantidade de erros armazenados e apresentados.
Retomada
Para imports longos, registre offset lógico, número da linha ou ID processado. Retomar por byte é difícil com compressão e Unicode; uma staging table pode ser mais confiável.
Compressão
NDJSON comprime bem porque repete nomes de campos. Use gzip ou Brotli em streaming.
const compressed = Readable.toWeb(source)
.pipeThrough(new CompressionStream('gzip'));Veja CompressionStream no Node.js.
NDJSON em logs
Um evento JSON por linha facilita ferramentas Unix e ingestão por agentes. Garanta que strings sejam serializadas por JSON.stringify, impedindo que quebras de linha internas criem registros falsos.
Ordenação e idempotência
Inclua ID estável por registro. Reprocessamento pode acontecer depois de falha. Use upsert ou tabela Inbox para impedir duplicação.
Segurança
- limite corpo, linha e registros;
- valide Content-Type;
- aplique autenticação e tenant;
- não permita propriedades perigosas;
- normalize strings quando necessário;
- não execute valores como código;
- use timeout e cancelamento;
- evite logs com dados completos.
Observabilidade
Monitore bytes, linhas, registros válidos, rejeitados, velocidade, duração por lote e memória. Inclua job ID e tenant nos logs.
Testes
- arquivo vazio;
- última linha sem newline;
- CRLF;
- Unicode dividido entre chunks;
- linha inválida;
- linha gigante;
- registro fora do schema;
- cancelamento;
- falha no meio do lote;
- reprocessamento idempotente.
Erros comuns
- carregar arquivo inteiro;
- não respeitar backpressure;
- não limitar linha;
- usar split simples em arquivo enorme;
- não validar registros;
- manter uma transação longa;
- não definir política de erros;
- confundir NDJSON com array JSON;
- não tratar Unicode incremental.
Checklist
- Content-Type definido;
- streaming real;
- limites de bytes e linhas;
- decoder incremental;
- schema por item;
- lotes controlados;
- política de atomicidade;
- checkpoint;
- idempotência;
- métricas de progresso.
Conclusão
NDJSON no Node.js é uma solução simples para transferir e processar grandes coleções de objetos JSON. O formato trabalha naturalmente com streams e permite validar cada registro de forma independente.
Preserve backpressure, limite linhas e use lotes transacionais. Com checkpoints, relatórios de erro e idempotência, NDJSON atende exportações e imports de grande volume sem transformar memória em gargalo.
Consulte a especificação NDJSON e a documentação de Streams do Node.js.



