FHIR API Design Patterns for 2026
FHIR (Fast Healthcare Interoperability Resources) is the standard for healthcare API design. It's also one of those standards where reading the spec gives you 30% of the working knowledge you need; the other 70% comes from shipping it.
This is a practical patterns guide based on FHIR APIs we've built and consumed in production — what we do, what we don't do, where the spec helps, and where the gap between FHIR theory and EHR-vendor reality forces design decisions the spec doesn't address.
Key Takeaways
- Pin to FHIR R4 if you're consuming any major EHR (Epic, Cerner/Oracle Health, Athenahealth) in 2026. R5 is published but not yet universal in production. R4B is R4 with backward-compatible patches.
- Use US Core profiles, not vanilla FHIR. ONC requires US Core for certified EHR APIs in the US. Publish your supported profiles via a
CapabilityStatementresource at/metadata. - Reference resources by full canonical URL —
{"reference": "Patient/abc-123"}— not bare IDs. This makes resources portable across systems and is required for clean Bundle transactions. Bundleis for atomic transactions, not just bulk reads. When you need Patient + Encounter + Observation written together, atransactionBundle is the right primitive — not three separate API calls with cleanup logic on failure.- The spec describes a clean FHIR. EHRs implement subsets, dialects, and quirks. Parse leniently, log every shape you didn't expect, and keep a per-customer profile of quirks you've encountered.
When FHIR is the right answer (and when it isn't)
Before the patterns: a quick filter on whether FHIR is the right protocol for your integration.
Use FHIR when:
- The integration is patient-facing or clinician-facing inside an EHR workflow (SMART on FHIR launches)
- You're consuming a major EHR's API surface (Epic, Cerner/Oracle Health, Athenahealth — all have FHIR endpoints)
- The data maps cleanly to FHIR's resource model (Patient, Encounter, Observation, MedicationRequest, Condition, Procedure)
- You're integrating across multiple EHRs and want a common abstraction layer
Use HL7 v2 over MLLP / batch files when:
- Back-office system-to-system integration (lab → EHR, EHR → billing, ADT feeds)
- High-volume real-time message streams
- The customer's interface engine (Mirth, Rhapsody, Cloverleaf) expects HL7 v2
- The data class you need isn't well-covered in FHIR yet (some long-tail message types)
Use vendor proprietary APIs when:
- The vendor offers them and they expose data that's not in FHIR (custom reports, workflow triggers, document retrieval at speed)
Most production healthcare integrations run all three concurrently. Architecture should hide the protocol layer from the rest of the application.
R4 vs R4B vs R5 — what to actually target
FHIR has version churn. As of 2026:
- R4 is what every major EHR supports in production. If you're consuming an EHR's FHIR API, you're on R4.
- R4B is a maintenance release of R4 with breaking-change-tolerant clarifications. Adoption is partial; consider it R4 with patches.
- R5 is the current published normative version. EHR adoption is starting but not universal. Some startup health products are R5-native; most established platforms remain R4.
Practical default for new builds: support R4 inputs/outputs, design your internal resource models with R5 in mind so the migration is incremental. If you control both sides of the integration, R5 is fine. If you're talking to EHRs, R4.
Pin to a specific FHIR version in your API. Don't auto-negotiate — clients lose track of which version they're targeting and integrations break on schema drift.
Use profiles to constrain the spec
FHIR resources are flexibly designed. Patient has dozens of optional fields. Observation can carry almost anything. This flexibility is a feature for the spec and a problem for an API consumer who needs to write parsing code.
The solution: profiles. A FHIR profile constrains a base resource to a specific shape — which fields are required, which value sets are allowed, which extensions apply.
Use established profiles:
- US Core — the de-facto US healthcare interoperability profile set. Maintained by HL7. Required by ONC for certified EHR APIs. Default profile to target for US-market products.
- Argonaut — earlier profile set, largely superseded by US Core but still referenced.
- Specialty-specific profiles — Da Vinci profiles for payer/provider interactions, IPS (International Patient Summary), MedMorph for public health.
Build custom profiles when:
- Your product has a specific data shape that's not well-covered by an existing profile
- You want to enforce data quality constraints (e.g., "Observation.code must be a LOINC code from this specific value set")
Document your profiles in a published CapabilityStatement resource at /metadata. Clients can validate against it.
Resource modeling — the patterns that survive
Some FHIR resource modeling decisions ship cleanly. Others accumulate technical debt. Patterns we use:
Reference everything by canonical URL
When linking between resources, use absolute URLs that include the resource type, not just IDs:
{
"subject": {
"reference": "Patient/abc-123",
"display": "Jane Doe"
}
}
Avoid:
{
"subject": "abc-123"
}
The full reference makes the resource portable across systems. The display helps human reading without requiring a fetch.
Use identifier for external IDs, not id
The id field is your system's primary key for that resource. The identifier array is for cross-system identification — MRN at a specific hospital, NPI for a practitioner, etc. Each identifier has a system URI that namespaces it.
{
"resourceType": "Patient",
"id": "internal-uuid-12345",
"identifier": [
{
"system": "http://hospital.example.org/mrn",
"value": "MR-789456"
},
{
"system": "http://hl7.org/fhir/sid/us-ssn",
"value": "999-99-9999"
}
]
}
This pattern matters when the same patient exists in multiple systems and you need to correlate them without losing track of which identifier came from which source.
Don't abuse extension — but use it when needed
FHIR's extension mechanism lets you add fields the base resource doesn't have. Tempting to use as a dumping ground.
Use extensions when:
- The data is genuinely structured and meaningful (e.g., a custom risk score that doesn't fit any standard FHIR field)
- The extension is well-defined (URL, value type, cardinality documented)
- Multiple consumers will read it consistently
Don't use extensions when:
- The data is essentially free-form notes — use a
NoteorAnnotationelement instead - A standard FHIR resource exists for what you're modeling — use that resource, not Patient + extension
Extensions are versioned by URL. Pick stable URLs in a domain you control. Don't change the meaning of an extension after it's in the wild.
Bundle for transactions, not just bulk
The Bundle resource serves multiple purposes:
searchset— a search result. Read-only, contains matching resources with paging links.transaction— an atomic write across multiple resources. Either all succeed or all roll back.batch— a non-atomic batch of writes. Each succeeds or fails independently.history— historical versions of resources.
The transaction bundle is underused. When you need to create a Patient + Encounter + Observation in a single atomic operation, a transaction bundle is the right primitive. Don't make three separate API calls and try to clean up if one fails.
Search parameter design
FHIR's search syntax is rich and well-specified. The patterns that matter in practice:
- Support the standard search parameters for every resource you expose. Patient should support
?name=,?identifier=,?birthdate=,?gender=, etc. If you don't, clients fall back to client-side filtering of full result sets — which kills performance. - Implement
_includeand_revincludecarefully. They let clients pull related resources in one round trip. Powerful but easy to abuse — limit how many levels deep, what types are allowed, and what page sizes apply. - Implement
_countand_paginationcorrectly. The standard pagination links (next,previous,first,last) need to work. Many FHIR implementations get this wrong; clients then write defensive code that doesn't trust the pagination. - Use
_summary=trueto return lightweight representations. Useful when clients are paging through results and don't need the full resource on each one. - Use modifiers thoughtfully:
:containsfor partial string match,:exactfor full-match,:missing=trueto find resources missing a field,:notfor exclusion.
SMART on FHIR launch flows
SMART on FHIR is OAuth2 with healthcare-specific extensions. Three patterns we ship most often:
EHR launch
User is inside the EHR; clicks a button to launch your app. The EHR sends a launch parameter your app exchanges for an authorization code, which becomes an access token scoped to the patient/encounter context.
The complications: the launch URL fires before some EHRs' auth servers are ready; the launch context can be incomplete; the user's EHR session can expire during the OAuth dance.
Build state management that survives these. Every launch should produce diagnostic state (even one that fails) for support.
Standalone launch
User is in your app first, not the EHR. Your app initiates OAuth against the EHR's authorization server. The user is redirected to the EHR for authentication, then back. Patient context may or may not be pre-selected.
This flow is more forgiving but requires more thoughtful UX. Users may not understand they're being asked to authenticate against the EHR.
Backend services
No user — system-to-system. Your service authenticates via JWT signed with a registered private key. Useful for bulk data export, scheduled reporting, server-to-server data sync.
The key management for backend services authentication is operationally non-trivial. Rotate keys; have a process; document it.
Versioning your own FHIR API
When you expose your own FHIR API to partners or your own apps, version it. Patterns:
- Pin to a specific FHIR version per endpoint.
/fhir/r4/...and/fhir/r5/...are separate. Don't promise content negotiation that you'll never maintain. - Publish a
CapabilityStatement. Clients can introspect your API. - Use ETags on individual resources so clients can do conditional updates and avoid lost-update races.
- Support resource history (
_historyendpoint) when clients need it for audit or undo workflows. - Version your custom profiles by URL. Don't change the meaning of a profile in place.
Surviving the gap between spec and reality
The spec describes a clean FHIR. The EHRs implement subsets, dialects, and quirks. Things we've encountered:
- Returning resources that don't strictly validate against any published profile
Observation.code.coding[].systemURIs that vary per customer instance of the same EHR- Required fields that are sometimes empty
- Search parameters documented as supported but returning unexpected results
- Pagination links that don't actually paginate correctly
Defensive patterns:
- Parse leniently; log when you encounter shapes you didn't expect
- Treat the EHR's response as input you need to validate, not data you can trust
- Monitor data-shape distributions over time so you notice when a customer upgrade changes their FHIR responses
- Keep a per-customer profile of quirks you've encountered
What we ship for FHIR clients
When we build a product that consumes FHIR endpoints, the architecture typically looks like:
Application
↓ (typed domain model)
FHIR Client Service
↓ (versioned per EHR, profile-aware)
EHR FHIR endpoint
The FHIR Client Service handles versioning, profile validation, retries, idempotency, and the quirks. The application sees a clean domain model. Switching FHIR versions or adding a new EHR doesn't require touching product code.
What we ship for FHIR servers
When we expose our own FHIR endpoints, we usually start with an off-the-shelf base (HAPI FHIR for Java stacks, Aidbox or Medplum for greenfield) and customize from there. Writing a FHIR server from scratch is a multi-quarter project; standing on existing implementations and customizing is faster and gets you the spec-compliance battle-tested.
If you're working on FHIR integration, designing a FHIR API, or modernizing an older healthcare integration to FHIR, we'd be glad to help. See our healthcare software development services for how we approach regulated builds end-to-end, our EHR integration guide for the broader EHR/EMR integration patterns, and our HIPAA-compliant AI architecture guide for the AI side of the same problem.