npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fhir-validator-mx

v0.1.7

Published

FHIR R4 resource validator with Dutch nl-core profile support and Nictiz terminologieserver integration

Readme

fhir-validator-mx

A TypeScript/Node.js library for validating FHIR R4 resources against StructureDefinition profiles, with support for Dutch (nl-core) healthcare standards and the Nictiz terminologieserver.

Features

  • Profile-based validation — validate resources against FHIR StructureDefinition profiles (snapshot and differential)
  • Cardinality checks — enforces min/max element constraints
  • Type validation — verifies FHIR primitive and complex types including date/dateTime/instant/time format validation
  • Terminology binding — validates codes against ValueSets and CodeSystems (local + art-decor + Nictiz + optional external tx server)
  • Art-decor auto-resolution — automatically fetches missing ValueSets and CodeSystems from decor.nictiz.nl at runtime
  • Nictiz terminologieserver — OAuth2 integration with the Dutch national terminology server for SNOMED CT NL validation
  • Extensible binding support — codes from systems outside the ValueSet are accepted for extensible/preferred bindings (FHIR spec)
  • System aliases — maps well-known OID URNs to canonical HL7 URLs (e.g. urn:oid:1.0.639.1 to iso639-1), plus STU3↔R4 URL normalization (http://hl7.org/fhir/v3/Xhttp://terminology.hl7.org/CodeSystem/v3-X)
  • FHIRPath constraints — evaluates FHIRPath invariant expressions
  • Slicing support — handles discriminated slicing (value, type, pattern discriminators)
  • Fixed & pattern value checks — enforces fixedCoding, patternIdentifier, etc.
  • Lazy loading with file index — builds a lightweight index on first run, loads files on demand (~230x faster startup)
  • Art-Decor disk cache — caches HTTP responses to disk, eliminating repeated network calls (~30x faster batch validation)
  • Preload for batch modepreload() loads all indexed files in parallel for maximum throughput
  • MongoDB source — load conformance resources from MongoDB instead of (or alongside) the filesystem, with automatic persistence of externally resolved resources
  • Multi-directory loading — load profiles and terminology from multiple directories in order (base first, overlays second)
  • BSN elfproef — validates Dutch citizen service numbers using the 11-test algorithm
  • Security — prototype pollution detection, error message sanitization (no PHI leakage)
  • Configurable severity — override issue severity per code (e.g. downgrade CODE_INVALID to warning)
  • FHIR version check — reject resources with incompatible FHIR version in meta.profile
  • Rate limiting — sliding window rate limit on external terminology calls
  • Batch validation — validate multiple resources in a single call
  • Validation metadata — each result includes a unique validationId (UUID) and timestamp

Validation Flow

Validation Flow

Installation

npm install fhir-validator-mx

This installs the validator library only. FHIR profiles and terminology data are not included — you provide your own (see Setting up FHIR definitions below).

Setting up FHIR definitions

The validator needs StructureDefinition, ValueSet and CodeSystem JSON files to validate against. You must download these yourself and point the validator at the directories containing them.

1. Download FHIR R4 core definitions

curl -LO https://hl7.org/fhir/R4/definitions.json.zip

# Extract StructureDefinitions
mkdir -p profiles/r4-core
unzip -j definitions.json.zip "profiles-resources.json" "profiles-types.json" -d /tmp/fhir-r4
node -e "
  const fs = require('fs');
  for (const f of ['profiles-resources', 'profiles-types']) {
    const bundle = JSON.parse(fs.readFileSync('/tmp/fhir-r4/' + f + '.json', 'utf8'));
    for (const entry of bundle.entry || []) {
      const r = entry.resource;
      if (r.resourceType === 'StructureDefinition') {
        fs.writeFileSync('profiles/r4-core/StructureDefinition-' + r.id + '.json', JSON.stringify(r, null, 2));
      }
    }
  }
"

# Extract ValueSets and CodeSystems
mkdir -p terminology/r4-core
unzip -j definitions.json.zip "valuesets.json" -d /tmp/fhir-r4
node -e "
  const fs = require('fs');
  const bundle = JSON.parse(fs.readFileSync('/tmp/fhir-r4/valuesets.json', 'utf8'));
  for (const entry of bundle.entry || []) {
    const r = entry.resource;
    if (r.resourceType === 'ValueSet') {
      fs.writeFileSync('terminology/r4-core/ValueSet-' + r.id + '.json', JSON.stringify(r, null, 2));
    } else if (r.resourceType === 'CodeSystem') {
      fs.writeFileSync('terminology/r4-core/CodeSystem-' + r.id + '.json', JSON.stringify(r, null, 2));
    }
  }
"

2. (Optional) Download nl-core profiles

The Dutch nl-core profiles are published by Nictiz on the Simplifier registry:

npm --registry https://packages.simplifier.net pack nictiz.fhir.nl.r4.nl-core

mkdir -p profiles/nl-core terminology/nl-core
tar xzf nictiz.fhir.nl.r4.nl-core-*.tgz
node -e "
  const fs = require('fs');
  for (const f of fs.readdirSync('package')) {
    if (!f.endsWith('.json')) continue;
    try {
      const r = JSON.parse(fs.readFileSync('package/' + f, 'utf8'));
      if (r.resourceType === 'StructureDefinition') {
        fs.copyFileSync('package/' + f, 'profiles/nl-core/' + f);
      } else if (r.resourceType === 'ValueSet' || r.resourceType === 'CodeSystem') {
        fs.copyFileSync('package/' + f, 'terminology/nl-core/' + f);
      }
    } catch {}
  }
"
rm -rf package nictiz.fhir.nl.r4.nl-core-*.tgz

3. Directory layout

After downloading, your directory structure should look like this:

your-project/
  profiles/
    r4-core/      — Base FHIR R4 StructureDefinitions (~658 files)
    nl-core/      — nl-core profile overlays (~164 files, optional)
  terminology/
    r4-core/      — Base FHIR R4 ValueSets and CodeSystems (~2378 files)
    nl-core/      — nl-core terminology (~8 files, optional)

Directories are loaded in order — base definitions first so that profile overlays can inherit from them.

Quick Start

import { FhirValidator } from 'fhir-validator-mx';

const validator = await FhirValidator.create({
  profilesDirs: ['profiles/r4-core', 'profiles/nl-core'],
  terminologyDirs: ['terminology/r4-core', 'terminology/nl-core'],
});

const result = await validator.validate({
  resourceType: 'Patient',
  identifier: [{ system: 'http://fhir.nl/fhir/NamingSystem/bsn', value: '999911120' }],
  name: [{ family: 'Jansen', given: ['Jan'] }],
  gender: 'male',
});

console.log(result.valid);        // true or false
console.log(result.validationId); // "a1b2c3d4-..."
console.log(result.issues);      // ValidationIssue[]

Batch Validation (with preload)

For validating many resources, call preload() after create() to load all profiles and terminology into memory in parallel:

const validator = await FhirValidator.create({
  profilesDirs: ['profiles/r4-core', 'profiles/nl-core'],
  terminologyDirs: ['terminology/r4-core', 'terminology/nl-core'],
});

await validator.preload(); // parallel async bulk load

const results = await validator.validateBatch(resources);

Event-Based Validation (ValidationRunner)

The ValidationRunner validates files or directories and emits typed events as each file is processed. It extends Node's EventEmitter, so there is no performance overhead — events are dispatched synchronously.

import { FhirValidator, ValidationRunner } from 'fhir-validator-mx';

const validator = await FhirValidator.create({
  profilesDirs: ['profiles/r4-core', 'profiles/nl-core'],
  terminologyDirs: ['terminology/r4-core', 'terminology/nl-core'],
});

const runner = new ValidationRunner(validator);

runner.on('pass', ({ file, result }) => {
  console.log(`PASS ${file}`);
});

runner.on('fail', ({ file, result }) => {
  console.log(`FAIL ${file}`);
  for (const issue of result.issues) {
    console.log(`  [${issue.severity}] ${issue.path}: ${issue.message}`);
  }
});

runner.on('error', ({ file, error }) => {
  console.log(`ERROR ${file}: ${error.message}`);
});

runner.on('progress', ({ current, total, file }) => {
  console.log(`${current}/${total} ${file}`);
});

runner.on('finish', (summary) => {
  console.log(`Done: ${summary.passed} passed, ${summary.failed} failed (${summary.elapsedMs}ms)`);
});

// Validate a directory or single file
const summary = await runner.run('path/to/resources/');

// Or validate an explicit list of files
const summary2 = await runner.validateFiles(['/path/to/Patient.json', '/path/to/Observation.json']);

Events

| Event | Payload | Description | |---|---|---| | pass | { file: string, result: ValidationResult } | Resource passed validation | | fail | { file: string, result: ValidationResult } | Resource failed validation (has errors) | | error | { file: string, error: Error } | File could not be read or parsed | | skip | { file: string, reason: string } | File skipped (e.g. no resourceType) | | progress | { current: number, total: number, file: string } | Emitted before each file is validated | | finish | RunnerSummary | All files processed |

The RunnerSummary returned by finish (and by the run()/validateFiles() return value) contains:

interface RunnerSummary {
  passed: number;
  failed: number;
  skipped: number;
  total: number;
  elapsedMs: number;
}

With Nictiz Terminologieserver

The validator can fall back to the Dutch national terminology server for codes that can't be validated locally (e.g. SNOMED CT codes referenced by broad ValueSets like observation-codes).

const config = await FhirValidator.loadConfig('config.local.json');

const validator = await FhirValidator.create({
  profilesDirs: ['profiles/r4-core', 'profiles/nl-core'],
  terminologyDirs: ['terminology/r4-core', 'terminology/nl-core'],
  terminology: {
    nictiz: config?.terminology,
  },
});

Copy config.example.json to config.local.json and fill in your Nictiz credentials. This file is gitignored.

Severity Overrides

const validator = await FhirValidator.create({
  profilesDirs: ['profiles/r4-core'],
  terminologyDirs: ['terminology/r4-core'],
  severityOverrides: {
    CODE_INVALID: 'warning', // downgrade binding errors to warnings
  },
});

Programmatic Registration

const validator = await FhirValidator.create();

validator.registerProfile({
  resourceType: 'StructureDefinition',
  url: 'http://example.org/fhir/StructureDefinition/MyPatient',
  name: 'MyPatient',
  status: 'active',
  kind: 'resource',
  abstract: false,
  type: 'Patient',
  snapshot: { element: [/* ... */] },
});

validator.terminology.registerValueSet({
  resourceType: 'ValueSet',
  url: 'http://hl7.org/fhir/ValueSet/administrative-gender',
  status: 'active',
  compose: { include: [/* ... */] },
});

MongoDB Source

Load conformance resources from a MongoDB collection instead of (or alongside) the filesystem. The mongodb driver is not bundled — you provide your own Collection instance.

import { MongoClient } from 'mongodb';
import { FhirValidator, MongoSource } from 'fhir-validator-mx';

const client = new MongoClient('mongodb://localhost:27017');
await client.connect();

const collection = client.db('fhir').collection('conformance_resources');
const validator = await FhirValidator.create({
  sources: [new MongoSource(collection)],
});

const result = await validator.validate(resource);
await client.close();

MongoDB and filesystem sources can be combined — filesystem directories are loaded first, then MongoDB resources are added on top:

const validator = await FhirValidator.create({
  profilesDirs: ['profiles/r4-core'],
  terminologyDirs: ['terminology/r4-core'],
  sources: [new MongoSource(collection)],
});

An optional filter restricts which documents are loaded:

const source = new MongoSource(collection, { status: 'active' });

Auto-persist externally resolved resources — When using a MongoDB source, ValueSets and CodeSystems fetched from external sources (Art-Decor, Nictiz) are automatically saved back to the MongoDB collection via upsert. This means the first validation run may fetch from the network, but subsequent runs find everything in the database.

Filesystem vs MongoDB Comparison

Validated 171 nl-core sample resources with identical profiles and terminology:

| | Filesystem | MongoDB | |---|---|---| | Conformance resources | ~900 (local JSON) | 3,279 | | Total time | 3,341ms | 3,349ms | | Avg per resource | 19.5ms | 19.6ms | | Passed | 171 | 171 | | Failed | 0 | 0 |

Performance is nearly identical. MongoDB loads all resources eagerly at create() time, while the filesystem uses lazy loading with an index. The MongoDB source provides a single centralized store that can be shared across services and automatically grows as new resources are resolved from external terminology servers.

Custom ResourceSource

Implement the ResourceSource interface to load from any backend:

import type { ResourceSource } from 'fhir-validator-mx';

class MyApiSource implements ResourceSource {
  async loadAll(): Promise<Record<string, unknown>[]> {
    const response = await fetch('https://my-fhir-server/conformance');
    return response.json();
  }

  async save(resource: Record<string, unknown>): Promise<void> {
    await fetch('https://my-fhir-server/conformance', {
      method: 'PUT',
      body: JSON.stringify(resource),
    });
  }
}

The optional save() method enables auto-persistence of externally resolved resources.

API

FhirValidator.create(options?)

Factory method that creates a validator and loads profiles/terminology.

| Option | Type | Description | |---|---|---| | profilesDirs | string[] | Directories containing StructureDefinition JSON files | | terminologyDirs | string[] | Directories containing ValueSet/CodeSystem JSON files | | sources | ResourceSource[] | Additional resource sources (e.g. MongoSource). Loaded eagerly at create time | | terminology.nictiz | NictizTerminologyConfig | Nictiz terminologieserver credentials | | terminology.externalTxServer | string | External terminology server URL (e.g. https://tx.fhir.org/r4) | | terminology.externalTimeoutMs | number | Timeout for external calls (default: 5000) | | terminology.externalRateLimit | number | Max external requests per minute (default: 30) | | terminology.disableExternalCalls | boolean | Block all external terminology calls | | terminology.artDecor.baseUrl | string | Art-decor FHIR base URL (default: https://decor.nictiz.nl/fhir) | | terminology.artDecor.timeoutMs | number | Art-decor fetch timeout (default: 10000) | | terminology.artDecor.disabled | boolean | Disable art-decor auto-resolution | | terminology.artDecor.cacheDir | string | Directory to cache Art-Decor HTTP responses on disk | | severityOverrides | Record<string, IssueSeverity> | Override severity per issue code | | fhirVersion | string | Accepted FHIR version (e.g. "4.0.1") | | indexCachePath | string | Path for the file index cache. If omitted, no index is written to disk | | eagerLoad | boolean | Force eager loading of all files instead of lazy loading (default: false) |

FhirValidator.loadConfig(path)

Loads terminology credentials from a JSON file. Returns null if the file does not exist.

validator.validate(resource, profileUrl?)

Returns a ValidationResult:

interface ValidationResult {
  valid: boolean;
  issues: ValidationIssue[];
  resourceType?: string;
  profile?: string;
  validationId?: string;
  timestamp?: string;
}

interface ValidationIssue {
  severity: 'error' | 'warning' | 'information';
  path: string;
  message: string;
  code?: string;
  expression?: string;
}

validator.preload()

Loads all indexed profiles and terminology into memory using parallel async reads. Call this after create() when validating many resources in batch mode.

validator.validateBatch(resources)

Validates an array of resources in parallel.

validator.stats()

Returns counts of loaded profiles, ValueSets, CodeSystems, cached terminology lookups, and whether Nictiz is configured.

Terminology Validation Cascade

When validating a code, the terminology service uses the following cascade:

  1. Local ValueSet — check expansion or compose against locally loaded ValueSets
  2. Art-decor auto-fetch — if the ValueSet URL is from decor.nictiz.nl, fetch and register it (with its CodeSystems)
  3. Extensible binding check — for extensible/preferred bindings, accept codes from systems not in the ValueSet
  4. Trusted system check — accept codes from well-known systems when no CodeSystem is available to validate against
  5. Nictiz fallback — if local validation fails, try the Nictiz CodeSystem/$lookup endpoint
  6. Local CodeSystem — validate directly against a locally loaded CodeSystem
  7. Art-decor CodeSystem — for urn:oid: systems, search art-decor for the CodeSystem
  8. Pattern validation — format checks for known systems (SNOMED, LOINC, BSN elfproef, AGB-Z, UZI, NPI)
  9. Trusted systems — always accept codes from well-known FHIR systems (administrative-gender, etc.)
  10. Nictiz CodeSystem — for completely unknown systems, try Nictiz
  11. Skip — if nothing can validate the code, accept it with a message

System Aliases

The validator maps well-known OID URNs to their canonical HL7 URLs before validation:

| OID / Legacy URL | Canonical URL | |---|---| | urn:oid:1.0.639.1 | http://terminology.hl7.org/CodeSystem/iso639-1 | | urn:oid:2.16.840.1.113883.6.121 | http://terminology.hl7.org/CodeSystem/iso639-2 | | http://hl7.org/fhir/v3/* (STU3) | http://terminology.hl7.org/CodeSystem/v3-* (R4) | | http://hl7.org/fhir/v2/* (STU3) | http://terminology.hl7.org/CodeSystem/v2-* (R4) |

This ensures that Dutch FHIR resources using OID-based system URLs or older STU3-style URLs match ValueSets that reference the canonical R4 URLs (and vice versa).

Using custom profiles

You can point the validator at any directory containing FHIR JSON files. For example, to add your own organization's profiles:

const validator = await FhirValidator.create({
  profilesDirs: [
    'profiles/r4-core',
    'profiles/nl-core',
    'profiles/my-organization',  // your custom profiles
  ],
  terminologyDirs: [
    'terminology/r4-core',
    'terminology/nl-core',
    'terminology/my-organization',  // your custom ValueSets/CodeSystems
  ],
});

The validator recursively scans all subdirectories for .json files.

Performance

The validator uses a multi-layered caching strategy to minimize startup time and avoid redundant I/O:

| Layer | What it does | Cache file | |---|---|---| | File index | Scans directories once, stores metadata (url, name, id, filePath) | opt-in via indexCachePath | | Lazy loading | Loads JSON files from disk only when resolve() or validateCode() needs them | in-memory | | Preload | Bulk-loads all indexed files in parallel (for batch scenarios) | in-memory | | validateCode cache | Deduplicates identical terminology lookups within a session | in-memory | | Art-Decor disk cache | Saves HTTP responses from decor.nictiz.nl to disk | opt-in via terminology.artDecor.cacheDir |

To enable disk caching for faster repeated startups and fewer network calls:

const validator = await FhirValidator.create({
  profilesDirs: ['profiles/r4-core', 'profiles/nl-core'],
  terminologyDirs: ['terminology/r4-core', 'terminology/nl-core'],
  indexCachePath: '.fhir-index.json',
  terminology: {
    artDecor: { cacheDir: '.art-decor-cache' },
  },
});

Benchmarks (822 profiles + 2386 terminology files, 171 sample resources)

| Scenario | create() | 171x validate() | Total | |---|---|---|---| | Eager loading (no cache) | 2129ms | ~65s | ~67s | | Lazy + cached index | 9ms | ~65s | ~65s | | Lazy + cached index + Art-Decor cache | 9ms | 2.2s | 2.2s |

The first run populates the Art-Decor disk cache (~65s with HTTP calls). Subsequent runs skip all network requests and complete in ~2 seconds.

CLI

Validate FHIR resources from the command line with colored output and progress tracking.

# Validate a single file
npm run validate -- samples/data/nl-core-Patient-01.json

# Validate all JSON files in a directory (with progress bar)
npm run validate -- samples/data/

# Custom profile and terminology directories
npm run validate -- samples/data/ --profiles-dir ./my-profiles --terminology-dir ./my-terminology

# Use a specific config file
npm run validate -- samples/data/ --config ./config.local.json

Output uses colors to indicate status: green for PASS, red for FAIL, yellow for warnings, and cyan for informational messages. When validating a directory, a progress bar shows completion status.

The exit code is 1 if any file fails validation, 0 if all pass.

Development

npm run build        # Webpack bundle + type declarations
npm run build:js     # Webpack bundle only
npm run build:types  # Type declarations only
npm test             # Run tests (36 tests)
npm run dev          # Run via ts-node
npm run lint         # ESLint
npm run validate     # CLI validator (see CLI section)

License

ISC