FHIRKit Client - v2.0.0
    Preparing search index...

    FHIRKit Client - v2.0.0

    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.

    • 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)
    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 zod
    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' },
    });

    @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