Stream a Bulk Data NDJSON export
A FHIR Bulk Data export arrives as NDJSON: one resource per line, often far more than fits in memory.
streamNdjson reads it as the chunks arrive and yields one record per line. A line that does not
read is reported with its line number, and the stream carries on with the next one.
Read the export a line at a time
Pass any iterable or async iterable of text or byte chunks: a Node.js stream from
fs.createReadStream, the body of a fetch response, or a generator. Each record carries its
1-based line and either the parsed resource or an error; a blank line yields nothing:
import { streamNdjson } from "@cosyte/fhir";
// Three chunks, split mid-line the way a network read splits them. Line 3 is blank, line 4 is broken.
async function* chunks(): AsyncGenerator<string> {
yield '{"resourceType":"Patient","id":"syn-0001"}\n{"resourceType":"Pat';
yield 'ient","id":"syn-0002"}\n\n{not json\n';
yield '{"resourceType":"Patient","id":"syn-0003"}\n';
}
const outcome: string[] = [];
for await (const record of streamNdjson(chunks())) {
outcome.push(
record.error === undefined
? `line ${String(record.line)}: ${record.resource?.kind ?? "none"}`
: `line ${String(record.line)}: ${record.error.code}`,
);
}
outcome; // => ["line 1: complex", "line 2: complex", "line 4: MALFORMED_JSON", "line 5: complex"]
From a file, pass createReadStream(path) from node:fs as the source, and the loop is the same.
The repository's examples/bundles-and-ndjson.ts
streams a committed synthetic export that way.
An error carries a code from NDJSON_ERROR_CODES and the line number, and never the line's text:
the bytes of a broken line are as likely to be patient data as anything else in the export.
Bound the memory one line can take
Between chunks the reader holds only the part of the current line that has no newline yet.
maxLineBytes, 16 MiB by default, bounds that part: once it passes the bound with no newline in
sight, the line is reported as LINE_TOO_LONG and the rest of it is skipped up to the next newline.
import { streamNdjson } from "@cosyte/fhir";
const long = JSON.stringify({ resourceType: "Patient", id: "x".repeat(200) });
const chunks = [long.slice(0, 120), `${long.slice(120)}\n{"resourceType":"Patient"}\n`];
const codes: string[] = [];
for await (const record of streamNdjson(chunks, { maxLineBytes: 100 })) {
codes.push(record.error?.code ?? "read");
}
codes; // => ["LINE_TOO_LONG", "read"]
Read a single line
When you split the lines yourself, parseNdjsonLine reads one, with the same isolation: it never
throws.
import { parseNdjsonLine } from "@cosyte/fhir";
parseNdjsonLine('{"resourceType":"Patient","id":"syn-0001"}', 1).resource?.kind; // => "complex"
parseNdjsonLine("{not json", 2).error?.code; // => "MALFORMED_JSON"
Reference
streamNdjson,NdjsonOptionsandNdjsonRecord.parseNdjsonLine,NdjsonErrorandNDJSON_ERROR_CODES.- Read Bundles and resolve references for the resources once they are read.