Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

NDJSON no Node.js: imports e exports em streaming

Atualizado em: 25 de outubro de 2026

Rack de servidores processando fluxos de dados no Node.js

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.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita