Node.js FHIR R4 client library — TypeScript-first, ESM-only, zero polyfills.
v2 requires Node 18+. It uses native
fetch,AbortController, andURLSearchParams. CommonJS (require) is not supported. See the migration guide if upgrading.
- Full TypeScript source — types included, no
@types/fhir-kit-clientneeded - 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
debugpackage - Minimal dependencies (only
agentkeepaliveanddebug)
npm install fhir-kit-client# 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 zodimport { 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' },
});@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);
}
}@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());
}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)`);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);
}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;
}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 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' });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 by id
await client.update({ resourceType: 'Patient', id: '123', body: updatedPatient });
// Conditional update
await client.update({
resourceType: 'Patient',
searchParams: { identifier: 'system|value' },
body: updatedPatient,
});await client.patch({
resourceType: 'Patient',
id: '123',
jsonPatch: [
{ op: 'replace', path: '/active', value: false },
{ op: 'add', path: '/name/-', value: { use: 'nickname', text: 'Jay' } },
],
});await client.delete({ resourceType: 'Patient', id: '123' });// 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 },
});await client.resourceSearch({ resourceType: 'Observation', searchParams: { patient: '123' } });
await client.systemSearch({ searchParams: { _type: 'Patient' } });
await client.compartmentSearch({
resourceType: 'MedicationRequest',
compartment: { resourceType: 'Patient', id: '123' },
});// 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 });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 });// Instance history
await client.history({ resourceType: 'Patient', id: '123' });
// Type history
await client.history({ resourceType: 'Patient' });
// System history
await client.history();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 });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'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' });// 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,
});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 });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'));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);
},
});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| 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 |
See the examples directory for runnable SMART App Launch and CDS Hooks examples.
FHIRKit Client welcomes community contributions. All participants must follow the Code of Conduct. See CONTRIBUTING.md for details.
MIT — Copyright (c) 2018 Vermonster LLC