@cosyte/fhir
Read real-world FHIR R4, keep every value exactly as it was written, and validate it against your profiles.
FHIR R4 for Node.js and TypeScript: a resource model that keeps every value exactly as written, JSON and XML codecs, layered validation, and a safety readout for status and negation.
Why this exists
Reading FHIR correctly takes more than a JSON parse: a decimal written 0.010 has to stay
0.010, an integer64 has to keep every digit, and a status, a modifier, or a negation must never be
skipped, because a missed refuted reads as its opposite. We built @cosyte/fhir for the engineer
who reads FHIR R4 from a server or an EHR (the format ONC HTI-1 binds §170.315(g)(10) to, with US
Core and SMART on FHIR) and has to get those values right without reading the specification first.
Type definitions alone describe a resource's shape and check nothing at run time, and JSON.parse
turns 0.010 into the number 0.01 and knows nothing about modifiers. This library reads, writes,
and validates FHIR R4 with zero runtime dependencies, and tells you with a coded reason what it could
not read safely.
Status
The 0.1 line. From 0.1 the public API is settled: the exported names, options, result shapes, and issue codes are the surface we keep stable. While the package is below 1.0, a breaking change ships in a minor version and is called out in the changelog. Upgrading from 0.0.10 changes verdicts: R4's own constraints are now evaluated with no profile supplied, so a document that read valid can read invalid. The changelog sets out each change.
What it covers, against FHIR R4 (4.0.1): reading and writing JSON and XML into one immutable
model; validation in layers (structure, cardinality, value domains, R4's own constraints on the
eight modeled resource types, Quantity and UCUM, binding strength, and profiles you supply or author
with defineProfile, including their FHIRPath invariants); the readSafety and
assertSafeToSummarize safety readout; and Bundles, references and streaming Bulk Data NDJSON. R5
and DSTU2 documents can be read, since the model is schema-free, but they are not validated against
their own definitions.
Not covered yet: typed per-resource models (the built-in structural schemas cover the base elements
plus the eight modeled resource types), type and profile slicing discriminators (reported
PROFILE_SLICE_UNCHECKED, and the slices' own constraints are then not checked, since no occurrence
is assigned to them), FHIRPath outside the supported subset (reported
INVARIANT_UNCHECKED, never passed), bundled terminology or US Core content, spec-clean JSON written
from a model read out of XML, unit conversion, and executing a transaction Bundle.
Install
# pnpm (recommended). Also works with: npm install @cosyte/fhir | yarn add @cosyte/fhir
pnpm add @cosyte/fhir
Node.js 22 or later, tested on 22, 24, and 26. The package ships dual ESM and CommonJS builds with
type declarations for both, so import and require both work and neither needs a compatibility
shim. There are zero runtime dependencies, and it imports no Node built-in.
Usage
Read a document, keep its exact values, and get a verdict. The Observation below is synthetic: every value in it is fabricated, and the same document is committed as a test fixture in this repository.
import { parseResource, readObservationValue, validateResource } from "@cosyte/fhir";
const document = `{
"resourceType": "Observation",
"id": "syn-0001",
"status": "final",
"code": { "coding": [{ "system": "http://loinc.org", "code": "8480-6" }] },
"subject": { "reference": "Patient/syn-0001" },
"effectiveDateTime": "2026-01-05",
"valueQuantity": {
"value": 120.0,
"unit": "mmHg",
"system": "http://unitsofmeasure.org",
"code": "mm[Hg]"
}
}`;
const { resource, issues } = parseResource(document);
// The magnitude was written 120.0: the read keeps that exact form and says the protection mattered.
issues.map((issue) => issue.code); // => ["DECIMAL_PRECISION_AT_RISK"]
// Branch on the value[x] type before touching a magnitude, and compare on the UCUM code.
const reading = readObservationValue(resource);
reading?.type; // => "Quantity"
reading?.quantity?.value?.raw; // => "120.0"
reading?.quantity?.code; // => "mm[Hg]"
validateResource(resource).valid; // => true
The quickstart on docs.cosyte.com/fhir walks the same document step
by step, and examples/ holds four runnable
programs: read and validate, the safety readout, JSON and XML, and Bundles with Bulk Data NDJSON.
API
Everything ships from one entry point; there are no subpath imports.
- Read and write.
parseResourceandserializeResourcefor JSON,parseResourceXmlandserializeResourceXmlfor XML, over one schema-free model, andnodesEquivalentto compare a model read from one format with a model read from the other. Adecimalor aninteger64keeps its exact text (FhirDecimal,FhirInteger64) and never passes through a JavaScript number. - Validate.
validateResourcereturnsvalid, the issues, andtoOperationOutcome()to render them as anOperationOutcome. Pass theprofilesyou load withloadStructureDefinitionor author withdefineProfile, and aterminologyservice of your own for code validity. - Read status and negation.
readSafetysurfaces status, modifiers, and negations.assertSafeToSummarizethrowsFhirSafetyError, carrying locations only, when a resource holds something it cannot summarize honestly, such as amodifierExtensionit does not understand. - Read measured values.
readObservationValuebranches on thevalue[x]type and keeps the UCUMcode, andreadMedicationDosesreads dose quantities. No unit is ever converted. - Bundles and NDJSON.
readBundletells a transaction (all or nothing) from a batch and executes neither,resolveReferenceresolves relative, absolute, logical, and#fragmentreferences, andstreamNdjsonreads a Bulk Data export line by line, reporting a malformed line and carrying on.
PHI and safety
FHIR resources carry protected health information, so treat every document you hand this library as PHI.
- It works in memory. It reads, validates, and writes in memory and nothing else. It opens no socket, writes nothing to disk, and fetches nothing at run time; it has zero runtime dependencies, imports no Node built-in, and calls no terminology service unless you pass one in.
- Diagnostics carry no values. An issue is a code and a FHIRPath location, never the value it was raised over. A location can repeat an element name the document supplied when that name has the published form of a FHIR name, so treat a location as derived from the document.
- The XML reader accepts plain XML only. Anything else is refused with a typed
FhirXmlErrorthat carries a code and an offset into the input string (in UTF-16 code units, not bytes), never a slice of the document. The reader performs no I/O and resolves no URI. The troubleshooting pages on docs.cosyte.com/fhir list each refusal and its code. - Nesting is bounded. Both readers stop at a fixed depth with a typed error, so a deeply nested document fails cleanly rather than exhausting the stack.
- A status or a negation is not skipped silently.
assertSafeToSummarizerefuses a resource carrying amodifierExtensionit does not understand, and each other shape it cannot summarize honestly, rather than letting a summary walk past it. - What you still own. Transport, storage, retention, access control, audit logging, and every log line your own code writes. This library makes no compliance claim on your behalf.
Documentation
- docs.cosyte.com/fhir: installation, the quickstart, how-to guides, core concepts, current limits, troubleshooting, the API reference, and how we test the package and measure its performance.
documentation/capabilities.md: the full capability readout, layer by layer, with what each one checks and what it leaves open.documentation/in the repository: the capability readout, the FHIRPath coverage measured against the shared R4 conformance suite, and the architecture decisions.CHANGELOG.md: every change, with the breaking ones called out.
Support
Bug reports, questions, and FHIR quirks from the servers and EHRs you connect to belong in GitHub issues, with a minimal reproduction that uses synthetic data only. Ideas and open questions can also go to Discussions. Our support page lists every route.
Need a FHIR R4 integration built, or hosted and run for you? See our integration services.
Security
Report a vulnerability privately, never in a public issue: through GitHub's private vulnerability reporting on this repository, or by email to [email protected]. Use synthetic data only. Our security policy says what is in scope and which versions we support.
Contributing
Pull requests are welcome; every fixture must be synthetic, never a real patient's data.
pnpm install
pnpm build # dual ESM + CJS + .d.ts
pnpm typecheck
pnpm lint
pnpm test
Every meaningful change gets a changeset (pnpm changeset: minor for new capability or a breaking
change while the version is below 1.0, patch for a fix), and its summary becomes the change's
entry in CHANGELOG.md. See CONTRIBUTING.md.
License
MIT, copyright Cosyte LLC. See LICENSE. The names of the standards and code systems this package works with belong to their owners; see TRADEMARKS.md.