Skip to content

Vermonster/fhir-kit-client

FHIRKit Client

npm version Build Status GitHub license

API Documentation →

Node.js FHIR R4 client library — TypeScript-first, ESM-only, zero polyfills.

v2 requires Node 18+. It uses native fetch, AbortController, and URLSearchParams. CommonJS (require) is not supported. See the migration guide if upgrading.

Features

  • Full TypeScript source — types included, no @types/fhir-kit-client needed
  • All FHIR REST interactions (read, vread, create, update, patch, delete, history)
  • FHIR search: resource, compartment, system (GET and POST forms)
  • FHIR operations ($everything, $validate, etc.)
  • Batch and transaction bundles
  • Reference resolution: absolute, relative, in-bundle, and contained (#)
  • SMART App Launch — authorization URL discovery via capability statement or .well-known
  • Capability-checking tool (CapabilityTool)
  • Pagination helpers (nextPage / prevPage)
  • Custom request signer hook (AWS SigV4, HMAC, etc.)
  • Bearer token support
  • Debug logging via the debug package
  • Minimal dependencies (only agentkeepalive and debug)

Installation

npm install fhir-kit-client

Optional: TypeScript type packages

# Ambient FHIR R4/R4B/R5 namespace types (fhir4.Patient, fhir4.Bundle, …)
npm install --save-dev @types/fhir

# Runtime Zod schemas + inferred TypeScript types
npm install @reasonhealth/fhir-zod zod

Quick Start

import { Client } from 'fhir-kit-client';

const client = new Client({ baseUrl: 'https://r4.smarthealthit.org' });

// Read a patient
const patient = await client.read({ resourceType: 'Patient', id: '123' });
console.log(patient.resourceType); // 'Patient'

// Search
const bundle = await client.search({
  resourceType: 'Patient',
  searchParams: { name: 'Smith', _count: '10' },
});

TypeScript Types

With @types/fhir (ambient namespace types)

@types/fhir adds ambient globals like fhir4.Patient, fhir4.Bundle, etc. Use a type guard to narrow the generic FhirResource returned by the client:

import { Client } from 'fhir-kit-client';

const client = new Client({ baseUrl: 'https://r4.smarthealthit.org' });

function isPatient(r: fhir4.Resource): r is fhir4.Patient {
  return r.resourceType === 'Patient';
}

function isBundle(r: fhir4.Resource): r is fhir4.Bundle {
  return r.resourceType === 'Bundle';
}

// Read and narrow
const resource = await client.read({ resourceType: 'Patient', id: '123' });
if (isPatient(resource)) {
  // resource is now fhir4.Patient
  console.log(resource.name?.[0]?.family);
}

// Search and iterate bundle entries
const result = await client.search({
  resourceType: 'Observation',
  searchParams: { patient: '123', _count: '20' },
});
if (isBundle(result)) {
  for (const entry of result.entry ?? []) {
    console.log(entry.resource?.resourceType, entry.resource?.id);
  }
}

With @reasonhealth/fhir-zod (runtime validation + inferred types)

@reasonhealth/fhir-zod provides Zod schemas generated from official FHIR StructureDefinitions. Use them to validate server responses at runtime and get fully-typed resources without @types/fhir.

import { Client } from 'fhir-kit-client';
import { PatientSchema, BundleSchema, ObservationSchema } from '@reasonhealth/fhir-zod/r4';
import type { z } from 'zod';

type Patient = z.infer<typeof PatientSchema>;
type Bundle  = z.infer<typeof BundleSchema>;

const client = new Client({ baseUrl: 'https://r4.smarthealthit.org' });

// Parse and validate — throws ZodError if the response doesn't conform
const raw = await client.read({ resourceType: 'Patient', id: '123' });
const patient: Patient = PatientSchema.parse(raw);
console.log(patient.name?.[0]?.family);

// Safe parse — inspect errors without throwing
const result = ObservationSchema.safeParse(
  await client.read({ resourceType: 'Observation', id: 'obs-1' })
);
if (result.success) {
  console.log('Status:', result.data.status);
} else {
  console.error('Invalid Observation:', result.error.flatten());
}

Validate a search Bundle

import { BundleSchema, PatientSchema } from '@reasonhealth/fhir-zod/r4';

const raw = await client.search({ resourceType: 'Patient', searchParams: { name: 'Smith' } });
const bundle = BundleSchema.parse(raw);

const patients = (bundle.entry ?? [])
  .map(e => e.resource)
  .filter((r): r is NonNullable<typeof r> => r?.resourceType === 'Patient')
  .map(r => PatientSchema.parse(r));

console.log(`Found ${patients.length} patient(s)`);

Discriminated union across resource types

import { z } from 'zod';
import { PatientSchema, PractitionerSchema, RelatedPersonSchema } from '@reasonhealth/fhir-zod/r4';

const SubjectSchema = z.discriminatedUnion('resourceType', [
  PatientSchema,
  PractitionerSchema,
  RelatedPersonSchema,
]);
type Subject = z.infer<typeof SubjectSchema>;

function parseSubject(raw: unknown): Subject {
  return SubjectSchema.parse(raw);
}

Using both @types/fhir and @reasonhealth/fhir-zod together

Use the Zod schema as a type guard that bridges to the ambient fhir4 namespace types:

import { PatientSchema } from '@reasonhealth/fhir-zod/r4';

function isValidPatient(resource: fhir4.Resource): resource is fhir4.Patient {
  return PatientSchema.safeParse(resource).success;
}

API Reference

new Client(config)

import { Client } from 'fhir-kit-client';
import type { ClientConfig } from 'fhir-kit-client';

const client = new Client({
  baseUrl: 'https://r4.smarthealthit.org',   // required
  bearerToken: 'eyJ...',                      // optional, sets Authorization header
  customHeaders: { 'X-Tenant': 'acme' },      // optional, sent with every request
  requestSigner: (url, init) => {             // optional, for custom auth (e.g. AWS SigV4)
    init.headers = { ...init.headers, 'X-Custom-Sig': sign(url) };
  },
});

Properties can be updated after construction:

client.baseUrl = 'https://other-server.org/fhir';
client.bearerToken = newToken;
client.customHeaders = { 'X-Tenant': 'new-tenant' };

Read

// Read a resource by type and id
const patient = await client.read({ resourceType: 'Patient', id: '123' });

// Read a specific version
const v1 = await client.vread({ resourceType: 'Patient', id: '123', version: '1' });

Create

const created = await client.create({
  resourceType: 'Patient',
  body: { resourceType: 'Patient', name: [{ family: 'Smith', given: ['Jane'] }] },
});

// With Prefer: return=minimal (server returns 201 with empty body)
const minimal = await client.create({
  resourceType: 'Patient',
  body: { resourceType: 'Patient', name: [{ family: 'Smith' }] },
  options: { headers: { Prefer: 'return=minimal' } },
});
const { response } = Client.httpFor(minimal);
console.log(response?.status);          // 201
console.log(response?.headers.get('Location')); // Location header

Update

// Update by id
await client.update({ resourceType: 'Patient', id: '123', body: updatedPatient });

// Conditional update
await client.update({
  resourceType: 'Patient',
  searchParams: { identifier: 'system|value' },
  body: updatedPatient,
});

Patch (JSON Patch, RFC 6902)

await client.patch({
  resourceType: 'Patient',
  id: '123',
  jsonPatch: [
    { op: 'replace', path: '/active', value: false },
    { op: 'add', path: '/name/-', value: { use: 'nickname', text: 'Jay' } },
  ],
});

Delete

await client.delete({ resourceType: 'Patient', id: '123' });

Search

// Resource-type search (GET)
const bundle = await client.search({
  resourceType: 'Patient',
  searchParams: { name: 'Smith', birthdate: 'lt1990-01-01', _count: '20' },
});

// System-wide search
const all = await client.search({ searchParams: { _type: 'Patient,Practitioner' } });

// Compartment search
const conditions = await client.search({
  resourceType: 'Condition',
  compartment: { resourceType: 'Patient', id: '123' },
});

// POST-based search (when params exceed URL length)
const postResult = await client.search({
  resourceType: 'Patient',
  searchParams: { identifier: longList },
  options: { postSearch: true },
});

Direct methods: resourceSearch, compartmentSearch, systemSearch

await client.resourceSearch({ resourceType: 'Observation', searchParams: { patient: '123' } });
await client.systemSearch({ searchParams: { _type: 'Patient' } });
await client.compartmentSearch({
  resourceType: 'MedicationRequest',
  compartment: { resourceType: 'Patient', id: '123' },
});

Operations

// System operation (POST)
await client.operation({ name: 'convert', input: bundle });

// Type-level operation (GET with params)
await client.operation({
  name: 'translate',
  resourceType: 'ConceptMap',
  method: 'GET',
  input: { url: 'http://example.com/map', code: '73211009', system: 'http://snomed.info/sct' },
});

// Instance-level operation
await client.operation({ name: 'everything', resourceType: 'Patient', id: '123' });
await client.operation({ name: 'apply',      resourceType: 'PlanDefinition', id: 'pd-1' });
await client.operation({ name: 'validate',   resourceType: 'Patient', input: rawPatient });

Batch and Transaction

const batchBundle = {
  resourceType: 'Bundle',
  type: 'batch',
  entry: [
    { request: { method: 'GET', url: 'Patient/123' } },
    { request: { method: 'GET', url: 'Observation?patient=123&_count=5' } },
  ],
};
const batchResult = await client.batch({ body: batchBundle });

const txBundle = { resourceType: 'Bundle', type: 'transaction', entry: [...] };
const txResult  = await client.transaction({ body: txBundle });

History

// Instance history
await client.history({ resourceType: 'Patient', id: '123' });

// Type history
await client.history({ resourceType: 'Patient' });

// System history
await client.history();

Pagination

let bundle = await client.search({
  resourceType: 'Patient',
  searchParams: { _count: '10' },
});

// Walk forward through all pages
while (bundle) {
  processBatch(bundle);
  bundle = await client.nextPage({ bundle }) ?? null;
}

// Or go backwards
const prevBundle = await client.prevPage({ bundle });

SMART App Launch — smartAuthMetadata

Discovers SMART authorization URLs from the .well-known/smart-configuration endpoint, the capability statement, or .well-known/openid-configuration. The first successful response wins (race).

import { Client } from 'fhir-kit-client';
import type { SmartAuthMetadata } from 'fhir-kit-client';

const client = new Client({ baseUrl: 'https://launch.smarthealthit.org/v/r4/fhir' });
const { authorizeUrl, tokenUrl, registerUrl } = await client.smartAuthMetadata();

console.log(authorizeUrl?.toString()); // 'https://.../authorize'
console.log(tokenUrl?.toString());     // 'https://.../token'

CapabilityStatement & CapabilityTool

import { Client, CapabilityTool } from 'fhir-kit-client';

const client = new Client({ baseUrl: 'https://r4.smarthealthit.org' });
const cs = await client.capabilityStatement();
const tool = new CapabilityTool(cs);

// Server-level
tool.serverCan('transaction');                          // boolean
tool.serverSearch('_id');                               // boolean
tool.supportFor({ capabilityType: 'interaction', where: { code: 'history-system' } });

// Resource-level
tool.resourceCan('Patient', 'create');                  // boolean
tool.resourceSearch('Patient', 'birthdate');            // boolean
tool.interactionsFor({ resourceType: 'Patient' });      // string[]
tool.searchParamsFor({ resourceType: 'Patient' });      // string[]
tool.resourceCapabilities({ resourceType: 'Patient' }); // raw capability object
tool.capabilityContents({ resourceType: 'Patient', capabilityType: 'conditionalDelete' });

Reference Resolution

// Absolute, relative, and in-bundle references
const referenced = await client.resolve({ reference: 'Patient/123' });
const absolute   = await client.resolve({ reference: 'https://server.org/fhir/Patient/456' });

// In-bundle or contained — supply the context bundle/resource
const contained = await client.resolve({
  reference: '#condition-1',
  context: patient,
});
const bundleRef = await client.resolve({
  reference: 'Patient/123',
  context: bundle,
});

Raw Request

const patient   = await client.request('Patient/123');
const deleted   = await client.request('Patient/123', { method: 'DELETE' });
const created   = await client.request('Patient', { method: 'POST', body: newPatient });

Inspecting the HTTP Request/Response

Every FHIR response object carries hidden __request and __response properties that expose the underlying Request and Response objects.

import { Client } from 'fhir-kit-client';

const result = await client.read({ resourceType: 'Patient', id: '123' });
const { request, response } = Client.httpFor(result);

console.log(request?.url);         // 'https://server.org/fhir/Patient/123'
console.log(response?.status);     // 200
console.log(response?.headers.get('etag'));

Custom Request Signer (AWS SigV4, HMAC, etc.)

import { Client } from 'fhir-kit-client';
import { SignatureV4 } from '@smithy/signature-v4';
import { Sha256 } from '@aws-crypto/sha256-browser';

const signer = new SignatureV4({
  credentials: fromNodeProviderChain(),
  region: 'us-east-1',
  service: 'healthlake',
  sha256: Sha256,
});

const client = new Client({
  baseUrl: 'https://healthlake.us-east-1.amazonaws.com/datastore/<id>/r4',
  requestSigner: async (url, options) => {
    const signed = await signer.sign({
      method: options.method ?? 'GET',
      headers: options.headers as Record<string, string>,
      hostname: new URL(url).hostname,
      path: new URL(url).pathname,
      protocol: 'https',
      body: options.body as string | undefined,
    });
    Object.assign(options.headers!, signed.headers);
  },
});

Logging

Uses the debug package.

Namespace Content
fhir-kit-client:info Every request URL and response status
fhir-kit-client:error Errors
# Enable all logging during development
DEBUG=fhir-kit-client:* node app.js

# Requests/responses only
DEBUG=fhir-kit-client:info node app.js

Migrating from v1

v1 v2
require('fhir-kit-client') import { Client } from 'fhir-kit-client'
Node 12+ Node 18+ required
cross-fetch, node-abort-controller polyfills Native fetch, AbortController
client.read({…, headers: {…}}) client.read({…, options: { headers: {…} }})
client.nextPage(bundle) client.nextPage({ bundle })
query-string (alpha sort) URLSearchParams (insertion order)
fhir-kit-client default export Named export Client

Examples

See the examples directory for runnable SMART App Launch and CDS Hooks examples.

Contributing

FHIRKit Client welcomes community contributions. All participants must follow the Code of Conduct. See CONTRIBUTING.md for details.

License

MIT — Copyright (c) 2018 Vermonster LLC

About

Node.js FHIR client library

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors