Skip to main content

US Core Compliance

Problem​

The ONC certification rules require US Core profiles, which add MustSupport cardinality constraints on top of base FHIR. If your code assumes Patient.gender is present (MustSupport) but the server returns a Patient without one, you need to surface the problem, not silently render undefined. Profile narrowing + client-side validation gives you both a tighter TypeScript shape and a runtime guard.

Prerequisites​

  • Generated client with a US Core IG: fhir-gen generate --version r4 --out ./src/fhir --ig hl7.fhir.us.core@6.1.0 --validator native
  • Packages: @fhir-dsl/core
  • Server: emits meta.profile on US Core resources and supports _profile search-param filtering

Steps​

1. Create the client with the generated schema registry​

The generator emits validators under ./fhir/r4/schemas/ conforming to Standard Schema v1. Wire them into the client as schemas on FhirClientConfig so .validate() has something to call.

import { createFhirClient } from "@fhir-dsl/core";
import { schemas } from "./fhir/r4/schemas/index.js";
import type { GeneratedSchema } from "./fhir/r4/client.js";

const fhir = createFhirClient<GeneratedSchema>({
baseUrl: "https://fhir.example/r4",
auth: { type: "bearer", credentials: process.env.TOKEN! },
schemas,
});

2. Narrow to the us-core-patient profile​

The second argument to search() is a generated profile name literal. It swaps the Prof type parameter so field access uses the profile's cardinality (e.g. gender becomes required) and _profile is filtered on the wire.

const page = await fhir
.search("Patient", "us-core-patient")
.where("family", "eq", "Jones")
.execute();

// TypeScript: `p.gender` is FhirCode (required), not FhirCode | undefined
for (const p of page.data) {
console.log(p.gender, p.identifier[0]?.value);
}

3. Run client-side validation with .validate()​

.validate() on a search is lazy — nothing happens until execute(). Each match-mode resource passes through the profile-level schema (or resource-level schema when no profile is selected).

import { ValidationError } from "@fhir-dsl/core";

try {
const page = await fhir
.search("Patient", "us-core-patient")
.where("family", "eq", "Jones")
.validate()
.execute();
return page.data;
} catch (e) {
if (e instanceof ValidationError) {
for (const issue of e.issues) {
console.error(issue.path?.join("."), "—", issue.message);
}
throw e;
}
throw e;
}

4. Select only MustSupport fields (_elements)​

US Core MustSupport means "render these when present". Pair profile narrowing with select() to both ask the server for only those fields and narrow the TypeScript shape through ApplySelection<Prof, Sel>.

const page = await fhir
.search("Patient", "us-core-patient")
.select(["id", "identifier", "name", "gender", "birthDate", "address", "telecom"])
.where("family", "eq", "Jones")
.execute();

// page.data: Array<Pick<USCorePatient, "resourceType" | "id" | ... >>

5. Fall back to server-side $validate for writes​

Before creating or updating a resource you're about to send to the server, ask the server to validate it against the profile. This catches vendor-specific terminology bindings your local validator can't see.

const outcome = await fhir.operation("$validate", {
scope: { kind: "type", resourceType: "Patient" },
parameters: {
resource: {
resourceType: "Patient",
meta: {
profile: ["http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"],
},
identifier: [{ system: "http://hospital.example/mrn", value: "MRN-42" }],
name: [{ family: "Doe", given: ["Jane"] }],
gender: "female",
birthDate: "1990-05-15",
},
mode: "create",
},
}).execute();
// outcome is an OperationOutcome — inspect .issue for errors/warnings

Final snippet​

import { createFhirClient, ValidationError } from "@fhir-dsl/core";
import { schemas } from "./fhir/r4/schemas/index.js";
import type { GeneratedSchema } from "./fhir/r4/client.js";

const fhir = createFhirClient<GeneratedSchema>({
baseUrl: "https://fhir.example/r4",
auth: { type: "bearer", credentials: process.env.TOKEN! },
schemas,
});

export async function searchConformantPatients(family: string) {
try {
const page = await fhir
.search("Patient", "us-core-patient")
.select(["id", "identifier", "name", "gender", "birthDate", "address", "telecom"])
.where("family", "eq", family)
.validate()
.execute();

return page.data;
} catch (e) {
if (e instanceof ValidationError) {
console.error("Non-conformant US Core response:", e.issues);
}
throw e;
}
}

export async function validateBeforeWrite(resource: unknown) {
return fhir.operation("$validate", {
scope: { kind: "type", resourceType: "Patient" },
parameters: { resource, mode: "create" },
}).execute();
}

Troubleshooting​

  • ValidationUnavailableError at execute() → you forgot to pass schemas in FhirClientConfig. The registry resolution happens at the moment you call execute(), not at builder construction.
  • .validate() passes locally but server $validate fails → your local native/zod schema validates structure; the server also checks terminology bindings. Trust the server for terminology.
  • Gender isn't narrowed to required → you probably dropped the profile argument: search("Patient") vs. search("Patient", "us-core-patient"). The generator only swaps Prof when the profile literal is supplied.
  • Too many _elements fields rejected → some servers cap _elements at 64. Drop non-MustSupport fields or split into two queries.
  • .validate() is too slow on large pages → validation awaits per resource; move to stream() + manual validateOne if throughput matters, or switch --validator zod → --validator native at generation time (native is faster on cold starts).