Query Patterns
Common query patterns across different FHIR resource types.
Observations
Search by Patient and Status
const result = await fhir
.search("Observation")
.where("patient", "eq", "Patient/123")
.where("status", "eq", "final")
.sort("date", "desc")
.count(20)
.execute();
for (const obs of result.data) {
console.log(obs.code?.text, obs.effectiveDateTime);
}
Search by Code (LOINC)
const result = await fhir
.search("Observation")
.where("code", "eq", "http://loinc.org|85354-9") // Blood pressure panel
.where("patient", "eq", "Patient/123")
.where("date", "ge", "2024-01-01")
.execute();
Vital Signs with US Core Profile
const vitals = await fhir
.search(
"Observation",
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-vital-signs"
)
.where("patient", "eq", "Patient/123")
.where("status", "eq", "final")
.sort("date", "desc")
.execute();
// vitals.data is typed as USCoreVitalSignsProfile[]
Encounters
Recent Encounters for a Patient
const result = await fhir
.search("Encounter")
.where("patient", "eq", "Patient/123")
.where("date", "ge", "2024-01-01")
.sort("date", "desc")
.count(10)
.execute();
Encounters by Status
const result = await fhir
.search("Encounter")
.where("status", "eq", "finished")
.where("class", "eq", "http://terminology.hl7.org/CodeSystem/v3-ActCode|AMB")
.execute();
Conditions
Active Problems for a Patient
const result = await fhir
.search("Condition")
.where("patient", "eq", "Patient/123")
.where("clinical-status", "eq", "active")
.execute();
for (const condition of result.data) {
console.log(condition.code?.coding?.[0]?.display);
}
Search by ICD-10 Code
const result = await fhir
.search("Condition")
.where("code", "eq", "http://hl7.org/fhir/sid/icd-10-cm|E11.9")
.execute();
Medication Requests
Active Medications for a Patient
const result = await fhir
.search("MedicationRequest")
.where("patient", "eq", "Patient/123")
.where("status", "eq", "active")
.sort("date", "desc")
.execute();
Multi-Resource Transactions
Create a Patient with an Observation
const bundle = await fhir
.transaction()
.create({
resourceType: "Patient",
name: [{ family: "Doe", given: ["Jane"] }],
gender: "female",
birthDate: "1990-01-15",
})
.create({
resourceType: "Observation",
status: "final",
code: {
coding: [
{
system: "http://loinc.org",
code: "8302-2",
display: "Body height",
},
],
},
valueQuantity: {
value: 165,
unit: "cm",
system: "http://unitsofmeasure.org",
code: "cm",
},
})
.execute();
Batch Update Multiple Resources
const bundle = await fhir
.transaction()
.update({
resourceType: "Patient",
id: "patient-1",
active: false,
})
.update({
resourceType: "Patient",
id: "patient-2",
active: false,
})
.delete("Observation", "obs-old-1")
.delete("Observation", "obs-old-2")
.execute();
Reverse Includes and Chained Searches
Patient with All Their Observations (_revinclude)
const result = await fhir
.search("Patient")
.where("family", "eq", "Johnson")
.revinclude("Observation", "subject")
.execute();
// result.data: Patient[]
// result.included: Observation[] referencing these patients
for (const obs of result.included) {
console.log(obs.resourceType, obs.code?.text);
}
Search Through References (Chained Parameters)
// Find observations where the patient's name is "Smith"
const result = await fhir
.search("Observation")
.whereChained("subject", "Patient", "family", "eq", "Smith")
.where("status", "eq", "final")
.execute();
// Find encounters where the practitioner is in a specific organization
const result = await fhir
.search("Encounter")
.whereChained("participant", "Practitioner", "name", "eq", "Dr. Jones")
.sort("date", "desc")
.execute();
Filter by Referencing Resources (_has)
// Find patients who have at least one blood pressure observation
const result = await fhir
.search("Patient")
.has("Observation", "subject", "code", "eq", "http://loinc.org|85354-9")
.where("active", "eq", "true")
.execute();
// Find patients with recent encounters
const result = await fhir
.search("Patient")
.has("Encounter", "subject", "date", "ge", "2024-01-01")
.execute();
Composite Search Parameters
Composite parameters let you search on multiple values simultaneously, ensuring they apply to the same logical record:
Observation by Code and Quantity
// Find observations with systolic blood pressure > 140
const result = await fhir
.search("Observation")
.whereComposite("code-value-quantity", {
code: "http://loinc.org|8480-6",
"value-quantity": "gt140",
})
.execute();
Combine Composite with Other Params
const result = await fhir
.search("Observation")
.where("patient", "eq", "Patient/123")
.where("status", "eq", "final")
.whereComposite("code-value-quantity", {
code: "http://loinc.org|8480-6",
"value-quantity": "5.4|http://unitsofmeasure.org|mg",
})
.sort("date", "desc")
.execute();
Composite parameters are different from using multiple .where() calls. Multiple .where() calls act as independent filters (AND), while a composite parameter ensures the component values match on the same element within a resource.
Projecting Fields
Use .select() to request only specific top-level fields. The query compiles to FHIR's _elements parameter and the result type narrows to match — useful for reducing payload size on mobile or for dashboards that only need a few fields.
const summary = await fhir
.search("Patient")
.where("active", "eq", "true")
.select(["id", "name", "birthDate"])
.count(100)
.execute();
// summary.data[0] is typed as { resourceType: "Patient"; id?: string; name?: HumanName[]; birthDate?: FhirDate }
Only top-level element names are accepted — _elements does not support nested paths. Calling .select() twice replaces the previous selection.
// Combine with includes — projection applies only to the primary resource,
// included resources are returned in full.
const { data, included } = await fhir
.search("Patient")
.select(["id", "name"])
.include("general-practitioner")
.execute();
Per the FHIR spec, servers return at least the requested elements and may return more. The narrowed TypeScript type reflects what you asked for, not what the server is guaranteed to return.
Composing Reusable Queries
Because builders are immutable, you can create reusable base queries:
// Base query for a patient's data
function patientData(patientId: string) {
return fhir
.search("Observation")
.where("patient", "eq", `Patient/${patientId}`)
.where("status", "eq", "final");
}
// Extend for specific use cases
const recentLabs = await patientData("123")
.where("category", "eq", "http://terminology.hl7.org/CodeSystem/observation-category|laboratory")
.where("date", "ge", "2024-01-01")
.sort("date", "desc")
.execute();
const allVitals = await patientData("123")
.where("category", "eq", "http://terminology.hl7.org/CodeSystem/observation-category|vital-signs")
.sort("date", "desc")
.execute();
This pattern works because where() returns a new builder. The patientData() function produces a fresh builder each time it's called, and subsequent .where() calls extend it without mutation.
Streaming Large Datasets
Use .stream() to iterate over results across all pages without loading everything into memory:
// Stream all active patients, page by page
for await (const patient of fhir.search("Patient").where("active", "eq", "true").stream()) {
console.log(patient.id, patient.name);
}
Stream with Cancellation
const controller = new AbortController();
// Stop after processing 1000 results
let count = 0;
for await (const obs of fhir.search("Observation").stream({ signal: controller.signal })) {
process(obs);
if (++count >= 1000) {
controller.abort();
break;
}
}
Stream with Filters
.stream() works with all query builder methods:
for await (const obs of fhir
.search("Observation")
.where("patient", "eq", "Patient/123")
.where("status", "eq", "final")
.sort("date", "desc")
.count(100) // page size
.stream()
) {
console.log(obs.code?.text, obs.effectiveDateTime);
}
See the Streaming & Lazy Loading guide for more details on streaming vs eager execution.
Working with Pagination (Runtime)
For lower-level control, use the runtime package's pagination utilities directly:
import { FhirExecutor, fetchAllPages, paginate } from "@fhir-dsl/runtime";
const executor = new FhirExecutor({
baseUrl: "https://your-fhir-server.com/fhir",
});
// Fetch all pages at once
const query = fhir.search("Patient").where("active", "eq", "true").compile();
const firstPage = await executor.execute(query);
const allPatients = await fetchAllPages(executor, firstPage);
// Or stream pages with an async generator
for await (const page of paginate(executor, firstPage)) {
console.log(`Processing ${page.length} patients`);
}
Advanced Search Patterns
OR across multiple values
// Either gender, in one round-trip
const result = await fhir
.search("Patient")
.whereIn("gender", ["male", "female"])
.execute();
// Patient?gender=male,female
Filter by missing data
// Patients with no recorded birthdate
const result = await fhir
.search("Patient")
.whereMissing("birthdate", true)
.execute();
Multi-hop chained search
// Observations whose Encounter's Patient is named "Smith"
const result = await fhir
.search("Observation")
.whereChain(
[["encounter", "Encounter"], ["subject", "Patient"]],
"name",
"eq",
"Smith",
)
.execute();
Transitive _include
// Walk MedicationRequest -> Medication -> Substance
const result = await fhir
.search("MedicationRequest")
.include("medication")
.include("medication", { iterate: true })
.execute();
POST _search for long URLs
// Bulk identifier lookup that wouldn't fit in a GET URL
const result = await fhir
.search("Patient")
.whereIn("identifier", manyMrns)
.usePost()
.execute();
Result-shaping with meta params
// Just the count -- no resources, no narrative
const { total } = await fhir
.search("Observation")
.where("patient", "eq", "Patient/123")
.summary("count")
.total("accurate")
.execute();
Server-side _filter expression
const result = await fhir
.search("Patient")
.filter("name eq 'Smith' and (birthdate gt 1990 or active eq true)")
.execute();
Composable Conditions (where(callback))
When OR spans different parameters or the conditions need to be nested, pass a callback to where(...). The compiler picks the most natural FHIR shape automatically -- comma-OR for the simple case, _filter for everything else.
Same-parameter OR (compiles to comma-join)
const result = await fhir
.search("Observation")
.where((eb) =>
eb.or([
["status", "eq", "final"],
["status", "eq", "amended"],
]),
)
.execute();
// → Observation?status=final,amended
OR across different parameters (compiles to _filter)
// Match either condition status — falls back to _filter
const result = await fhir
.search("Observation")
.where((eb) =>
eb.or([
["status", "eq", "final"],
["code", "eq", "http://loinc.org|85354-9"],
]),
)
.execute();
// → Observation?_filter=status eq 'final' or code eq 'http://loinc.org|85354-9'
Nested groups
// "subject is Patient/123 AND (status is final OR amended)"
const result = await fhir
.search("Observation")
.where((eb) =>
eb.and([
["subject", "eq", "Patient/123"],
eb.or([
["status", "eq", "final"],
["status", "eq", "amended"],
]),
]),
)
.execute();
Operators with no _filter equivalent (exact, above, below, of-type, text, identifier, code-text, missing) cannot appear inside an OR or nested group — use the positional where(...) form for those. See Functional where for the full operator-mapping table.
Conditional Clauses ($if and $call)
Every fluent builder exposes two Kysely-style composition primitives that let you wire optional clauses and reusable fragments without breaking out of the chain.
Conditionally append clauses with $if
async function searchPatients(filters: {
family?: string;
active?: boolean;
withObservations?: boolean;
}) {
return fhir
.search("Patient")
.$if(filters.family != null, (qb) => qb.where("family", "eq", filters.family!))
.$if(filters.active === true, (qb) => qb.where("active", "eq", "true"))
.$if(filters.withObservations === true, (qb) => qb.revinclude("Observation", "subject"))
.execute();
}
$if returns the same builder type via polymorphic this, so include narrowing, profile selection, and _elements projection all survive across chained $if calls.
Extract reusable fragments with $call
// Define once
const recentFinal = <T extends { where: any }>(qb: T) =>
qb.where("status", "eq", "final").where("date", "ge", "2024-01-01");
// Reuse anywhere
const obs = await fhir.search("Observation").$call(recentFinal).count(50).execute();
const enc = await fhir.search("Encounter").$call(recentFinal).execute();
$call always invokes the callback and returns whatever it returns — useful for extracting query fragments, or for piping a builder into a non-builder result (e.g. qb.$call((b) => b.compile())).
Combining both
const tenantScope = (tenantId: string) =>
<T extends { where: any }>(qb: T) =>
qb.where("organization", "eq", `Organization/${tenantId}`);
const result = await fhir
.search("Patient")
.$call(tenantScope("acme"))
.$if(searchTerm.length > 0, (qb) => qb.where("name", "contains", searchTerm))
.execute();
Error Handling
import { FhirError } from "@fhir-dsl/runtime";
try {
const patient = await fhir.read("Patient", "nonexistent").execute();
} catch (error) {
if (error instanceof FhirError) {
console.error(`Status: ${error.status} ${error.statusText}`);
// FHIR OperationOutcome details
for (const issue of error.issues) {
console.error(`${issue.severity}: ${issue.diagnostics}`);
}
}
}