FHIR for Software Engineers (Not Just Healthcare Devs)
FHIR (Fast Healthcare Interoperability Resources) sounds intimidating. It has its own specification site, a 400-page standard, and a community full of people who say "bundle" and "resource" like everyone knows what they mean.
Here's the thing: FHIR is just REST + JSON with a standardized schema. If you've built a REST API, you already know 80% of what you need. I'm going to show you the practical parts by building real queries against a FHIR server.
What FHIR actually is
FHIR defines a set of "resources" (think: database models) for healthcare data. Each resource has a type, a JSON schema, and a REST endpoint. That's it.
Common resources you'll use:
- Patient: name, DOB, identifiers, contact info
- MedicationRequest: a prescription (what drug, what dose, who prescribed it)
- MedicationStatement: what the patient says they're taking
- AllergyIntolerance: known allergies and adverse reactions
- Observation: lab results, vitals, anything measured
- Condition: diagnoses
Every FHIR server exposes these at predictable URLs:
GET /Patient/123 # Read one patient
GET /Patient?name=Smith # Search patients
GET /MedicationRequest?patient=123 # Get prescriptions for a patient
That's standard REST. No custom protocols, no SOAP, no HL7v2 pipe-delimited messages from 1987.
Querying a FHIR server
Any HTTP client works. Here's a real example using curl against a public FHIR server:
# Search for a patient by name
curl "https://hapi.fhir.org/baseR4/Patient?name=Smith&_count=3" \
-H "Accept: application/fhir+json"
The response is a Bundle -- FHIR's wrapper for collections:
{
"resourceType": "Bundle",
"type": "searchset",
"total": 1247,
"entry": [
{
"resource": {
"resourceType": "Patient",
"id": "12345",
"name": [
{
"family": "Smith",
"given": ["John", "Michael"]
}
],
"birthDate": "1985-03-15",
"gender": "male"
}
}
]
}
Key patterns:
-
resourceTypetells you what you're looking at -
entry[].resourceis the actual data - Names are arrays (people have multiple names -- maiden, married, legal)
-
givenis also an array (first + middle names)
The TypeScript client
Raw HTTP works, but for production code use a typed client. Here's how to set one up:
import Client from "fhir-kit-client";
const fhirClient = new Client({
baseUrl: "https://your-fhir-server.com/fhir",
});
// Read a single patient
const patient = await fhirClient.read({
resourceType: "Patient",
id: "12345",
});
console.log(patient.name[0].family); // "Smith"
console.log(patient.birthDate); // "1985-03-15"
Searching returns Bundles. You'll write this pattern constantly:
// Get all medications for a patient
const bundle = await fhirClient.search({
resourceType: "MedicationRequest",
searchParams: {
patient: patientId,
status: "active",
_count: "100",
},
});
// Extract resources from the bundle
const medications = (bundle.entry || [])
.map((entry: any) => entry.resource)
.filter((r: any) => r.resourceType === "MedicationRequest");
That bundle.entry || [] guard is important. Empty search results return a Bundle with no entry field, not an empty array. This trips up everyone at least once.
Understanding medication data
This is where it gets interesting (and messy). A MedicationRequest looks like this:
{
"resourceType": "MedicationRequest",
"id": "med-001",
"status": "active",
"intent": "order",
"medicationCodeableConcept": {
"coding": [
{
"system": "http://www.nlm.nih.gov/research/umls/rxnorm",
"code": "860975",
"display": "Metformin 500mg Oral Tablet"
}
],
"text": "Metformin 500mg"
},
"subject": {
"reference": "Patient/12345"
},
"dosageInstruction": [
{
"text": "Take 1 tablet by mouth twice daily",
"timing": {
"repeat": {
"frequency": 2,
"period": 1,
"periodUnit": "d"
}
},
"doseAndRate": [
{
"doseQuantity": {
"value": 500,
"unit": "mg",
"system": "http://unitsofmeasure.org"
}
}
]
}
]
}
The medication name can live in three places:
-
medicationCodeableConcept.text-- human-readable -
medicationCodeableConcept.coding[0].display-- from a coding system -
medicationReference.reference-- points to a separate Medication resource
You need to handle all three. In production, I normalize drug names by stripping dose info, salt forms, and route abbreviations:
function normalizeDrugName(name: string): string {
return name
.toLowerCase()
// Remove dose info: "metformin 500mg" -> "metformin"
.replace(/\s*\d+\.?\d*\s*(mg|mcg|g|ml|units?)\b.*/i, "")
// Remove salt forms: "metformin hydrochloride" -> "metformin"
.replace(
/\s+(hydrochloride|hcl|sulfate|sodium|potassium|acetate|besylate|maleate|fumarate|calcium|citrate)\b/gi,
""
)
// Remove route: "metformin po" -> "metformin"
.replace(/\s+(iv|im|po|pr|sl|td|top|inh)\b.*/i, "")
.trim();
}
normalizeDrugName("Metformin Hydrochloride 500mg BID");
// -> "metformin"
This matters because hospitals, pharmacies, and patient self-reports all describe the same drug differently. "Metformin 500mg," "metformin hydrochloride 500mg oral tablet," and "Glucophage 500" are all metformin. Without normalization, your comparison logic sees three different drugs.
Search parameters you'll actually use
FHIR search has its own syntax, but it maps to query parameters:
# Patients born after 1990
GET /Patient?birthdate=gt1990-01-01
# Active prescriptions for patient 123
GET /MedicationRequest?patient=123&status=active
# Allergies marked as high criticality
GET /AllergyIntolerance?patient=123&criticality=high
# Include the referenced medication resource in results
GET /MedicationRequest?patient=123&_include=MedicationRequest:medication
# Paginate with _count and _offset
GET /Patient?name=Smith&_count=20&_offset=40
The _include parameter is powerful. Without it, a MedicationRequest that uses medicationReference (instead of inline medicationCodeableConcept) just gives you a reference like Medication/abc. With _include, the server resolves the reference and returns the Medication resource in the same Bundle.
Date comparisons use prefixes: gt (greater than), lt (less than), ge, le, eq. birthdate=gt1990-01-01 means born after January 1, 1990.
Building on top of FHIR
Once you can read FHIR data, you can build tools. Here's a real-world example: a medication reconciliation system that compares what the hospital prescribed vs. what the pharmacy dispensed vs. what the patient reports taking.
async function reconcileMedications(patientId: string) {
// Pull from hospital EHR (MedicationRequest = prescriptions)
const prescribed = await fhirClient.search({
resourceType: "MedicationRequest",
searchParams: { patient: patientId, status: "active" },
});
// Pull from pharmacy (MedicationStatement = what was dispensed/taken)
const reported = await fhirClient.search({
resourceType: "MedicationStatement",
searchParams: { patient: patientId, status: "active" },
});
const prescribedMeds = extractMedNames(prescribed);
const reportedMeds = extractMedNames(reported);
// Find discrepancies
const missingFromPharmacy = prescribedMeds.filter(
(med) => !reportedMeds.some(
(r) => normalizeDrugName(r) === normalizeDrugName(med)
)
);
return {
prescribed: prescribedMeds,
reported: reportedMeds,
discrepancies: missingFromPharmacy,
};
}
This is the core of a medication safety tool. Patients fall through the cracks at care transitions -- discharged from hospital with new prescriptions, but the pharmacy never gets updated, or the patient stops taking something and nobody notices. FHIR gives you the data layer to catch these gaps programmatically.
Running your own FHIR server
For development, use HAPI FHIR -- the reference implementation:
docker run -p 8080:8080 hapiproject/hapi:latest
That gives you a full FHIR R4 server at http://localhost:8080/fhir with a web UI for browsing resources. Load test data with Synthea (synthetic patient generator):
# Generate 100 synthetic patients
java -jar synthea-with-dependencies.jar -p 100 --exporter.fhir.export true
# Upload the generated bundles
for f in output/fhir/*.json; do
curl -X POST "http://localhost:8080/fhir" \
-H "Content-Type: application/fhir+json" \
-d @"$f"
done
Synthea generates realistic patient histories -- medications, conditions, allergies, lab results -- all in FHIR format. I loaded 281 synthetic patients this way for testing. Each patient comes with a complete medical timeline you can query.
Things that tripped me up
Bundle pagination. Large result sets come back paginated. The Bundle has a link array with relation: "next" pointing to the next page. If you only process the first page, you'll silently miss data.
Reference resolution. FHIR uses references between resources: "subject": {"reference": "Patient/123"}. The reference is a relative URL, not an ID. If you're parsing references to extract IDs, split on / and take the last segment.
FHIR versions matter. R4 and R5 have different field names for some resources. Check which version your server runs (usually in the CapabilityStatement at /metadata). Most production servers are still on R4.
CodeableConcept vs. Coding. A CodeableConcept contains a text field (human-readable) and a coding array (machine-readable codes from systems like RxNorm, SNOMED, ICD-10). Always prefer the text field for display. Use coding when you need to match across systems.
Empty bundles. An empty search result is a Bundle with total: 0 and no entry field. Not entry: []. Guard against undefined.
When to use FHIR
FHIR makes sense when:
- You're integrating with healthcare systems (EHRs, pharmacies, labs)
- You need a standard data model for health data
- Multiple systems need to exchange patient information
- You're building tools that operate on medical records
It doesn't make sense when:
- You're building a simple health tracker app (just use your own schema)
- You don't need interoperability with existing healthcare systems
- Your data doesn't map to FHIR's resource types
The standard is designed for interoperability. If you're not integrating with external systems, the overhead of FHIR compliance isn't worth it.
Resources
- FHIR R4 Spec -- the actual standard, surprisingly readable
- HAPI FHIR -- Java-based reference server, runs in Docker
- Synthea -- synthetic patient generator
- fhir-kit-client -- TypeScript/JS FHIR client
- SMART on FHIR -- OAuth2 profile for FHIR apps (how real EHR integrations authenticate)
I build production AI systems, including healthcare tools. If you're working with medical data, I'm at astraedus.dev.
Top comments (0)