FHIR Explained: The API Standard Behind Modern Healthcare Data Exchange in 2026
Introduction
This article is the entry point to the FHIR part of my HL7 series, and it is linked from my DICOM series too, because sooner or later every imaging engineer meets a FHIR API. I first wrote it in 2023 as a short overview with a list of definitions. Since then I have published a full set of Java and .NET programming articles on FHIR, and it became obvious that this introduction was doing the one thing an introduction should never do: describing a standard for two thousand words without ever showing you what it looks like. So this is a rewrite for 2026, in the same shape as my refreshed HL7 v2, DICOM and IHE introductions - the same purpose, but with a real resource, real HTTP exchanges, the vocabulary an engineer needs, and the context that did not exist when FHIR was new.
Who this is for: software developers, integration engineers, analysts and architects who are about to build against, or expose, a FHIR API for the first time. No prior healthcare knowledge is assumed, but if you know what a REST API and a JSON document are, you already know half of FHIR. If you have worked with HL7 v2, the section on where each standard lives will tell you what carries over and what does not.
The short version
- FHIR (Fast Healthcare Interoperability Resources, pronounced "fire") is HL7's modern standard for exchanging healthcare data. It is not primarily a messaging standard: it defines a set of resources - Patient, Observation, Encounter, MedicationRequest and around 150 others - and a RESTful API for creating, reading, updating, searching and deleting them over HTTP, in JSON or XML.
- A resource is a small, self-describing JSON (or XML) document with an id, some metadata, an optional human-readable narrative and a set of typed elements. Resources point at one another with references, so a patient's record is a graph of resources, not one big document.
- The base specification is deliberately loose - most elements are optional - so real-world use is governed by profiles and implementation guides (US Core, the International Patient Summary, the pan-Canadian profiles, IHE's FHIR profiles). "FHIR compliant" on its own means very little; "conforms to US Core 6.1" means a lot.
- The version you will meet in production in 2026 is overwhelmingly R4 (4.0.1, 2019), because that is what regulators mandated. R5 (2023) exists and R6 is in ballot, but neither has displaced R4.
- FHIR did not replace HL7 v2 or DICOM. It dominates APIs, apps and cross-organisation exchange; v2 still carries the feeds inside the hospital and DICOM still carries the images. You will need all three, and FHIR is the one that ties them together for the outside world.
“Simplicity is prerequisite for reliability.” ~ Edsger W. Dijkstra
What FHIR Is - and What It Is Not
Health Level Seven International (HL7) is the standards body behind the v2 messaging standard that has run hospitals since the late 1980s, and behind the v3 and CDA document standards of the 2000s. By 2011 it had a problem. v2 was everywhere but showed its age: every interface was a negotiation over optional fields and Z-segments. v3, the intended successor, was built on an elaborate Reference Information Model that took years to learn and never achieved broad adoption outside CDA documents and a few national programmes (I cover that story in my HL7 v3 article). Meanwhile, outside healthcare, the web had settled on a simple recipe for exchanging data between systems: HTTP, REST, JSON, OAuth.
In August 2011 Grahame Grieve, an Australian HL7 member, published a proposal called Resources for Health that asked a simple question: what if healthcare data were exchanged the way the rest of the web exchanges data? HL7 adopted it, renamed it FHIR, and published the first Draft Standard for Trial Use in 2014. The design goals were explicit, and they explain most of what you will see:
- Implementer first. The specification is written for developers, it is free to use under a Creative Commons licence, and every resource page has examples you can copy. A working FHIR client is an afternoon's work in any language with an HTTP library.
- Small, composable resources instead of one big model. Each resource covers one concept - a patient, an observation, an encounter - and covers only the common cases. The stated rule of thumb is that the base resource should handle 80% of implementations; the remaining 20% use extensions.
- Built on the web. HTTP is the transport, REST the interaction style, JSON and XML the formats, OAuth 2.0 the security model. FHIR invented as little as possible.
- Human-readable as well as machine-readable. Every resource can carry a narrative - an XHTML summary that a clinician can read even when the receiving system does not understand every coded element.
Two things FHIR is not. It is not a database schema or a clinical data model - resources describe what is exchanged, and how you store it internally is your business. And it is not only REST: the specification defines four exchange paradigms - REST (the API most people mean when they say "FHIR"), messaging (event-driven, v2-style, using a Bundle of type message), documents (a Bundle of type document with a Composition, the CDA successor), and services (operations like $validate or decision-support calls). In practice REST accounts for the overwhelming majority of what you will build, and it is what this article concentrates on.
Versions: Why R4 Is Still the One You Meet
FHIR has had six major releases in twelve years, and unlike HL7 v2 - where versions are close enough that a 2.3 message is usually readable by a 2.5.1 system - FHIR versions are not wire-compatible with one another. A resource that validates against R4 may be rejected by an R5 server, and the reverse. So the first question about any FHIR API is "which version?", and the answer, in 2026, is nearly always R4.
| Release | Published | Status | Where you meet it |
|---|---|---|---|
| DSTU1 (0.0.82) | Feb 2014 | Retired | Nowhere today; historical interest only |
| DSTU2 (1.0.2) | Oct 2015 | Retired | The first widely deployed version - the Argonaut Project, early SMART apps and the first EHR vendor APIs were built on it. A few long-lived integrations still run it |
| STU3 (3.0.2) | Mar 2017 | Retired | Some national programmes and older vendor APIs; migrating to R4 |
| R4 (4.0.1) | Dec 2018 / Oct 2019 | Normative core | The default. Mandated by the US ONC Cures Act rules (via US Core), adopted by most national programmes, supported by every major EHR and cloud FHIR service. First release with normative (never to break) content: the RESTful API, the formats, the data types, Patient, Observation, and the conformance resources |
| R4B (4.3.0) | May 2022 | Trial Use | A bridge release that back-ported a few R5 items (medication definitions, subscription topics) onto R4. Rarely targeted directly |
| R5 (5.0.0) | Mar 2023 | Trial Use | New resources and a redesigned subscriptions framework; some breaking changes (e.g. the CodeableReference data type, renamed resources). Supported by the main servers and libraries, chosen by some new greenfield projects, but not mandated anywhere significant yet |
| R6 (6.0.0) | In ballot | Normative ballot | HL7 has been balloting R6 as a full normative standard through 2026, with publication expected in 2026-27. Expect several years before it appears in regulation or in your production integrations |
Why did R4 stick? Timing. R4 was published just as regulators decided that FHIR should be mandatory. In the United States, the ONC 21st Century Cures Act Final Rule (2020) required every certified EHR to expose a FHIR R4 API conforming to US Core by the end of 2022, and the parallel CMS Interoperability and Patient Access rules required payers to do the same; the later CMS prior-authorisation rule (2024) added more R4-based APIs. Other countries followed with R4 profiles: the pan-Canadian CA Core work that I cover in my Java and .NET articles, the NHS's UK Core, Germany's ISiK, Australia's AU Core, and the European Health Data Space's reliance on the International Patient Summary. Once a version is written into regulation and into hundreds of implementation guides, moving off it is a decade-scale project - exactly what happened with HL7 v2.5.1.
Within a release, every resource and page also carries a Maturity Level (FMM 0 to 5, then Normative). Patient and Observation are normative; a resource at FMM 1 may change substantially in the next release. Check the little badge at the top of each resource page before you build on it.
The Mental Model: Resources, Elements, References, Extensions
Everything in FHIR is built from a handful of ideas. Once they click, the specification stops looking like 150 unrelated pages and starts looking like one system.
Resources
A resource is the unit of exchange: the smallest thing that can be independently identified, retrieved and updated. Every resource has a type (Patient, Observation), a logical id assigned by the server that holds it, a meta element (version id, last-updated time, profiles it claims to conform to, security labels, tags), an optional narrative, and then the elements specific to its type. The resource list groups them into five layers: Foundation (the infrastructure - Bundle, OperationOutcome, Binary, CapabilityStatement), Base (people, places and things - Patient, Practitioner, Organization, Location, Device, Encounter), Clinical (Observation, Condition, Procedure, MedicationRequest, DiagnosticReport, AllergyIntolerance, Immunization, CarePlan...), Financial (Coverage, Claim, ExplanationOfBenefit...) and Specialized (research, quality measures, clinical reasoning, medication definitions). Most developers use twenty of them regularly and look the rest up.
Elements and data types
Each resource is a tree of elements, and every element has a name, a cardinality (0..1, 1..1, 0..*) and a data type. The base specification keeps almost every element optional - Patient has exactly zero required elements - which is why profiles exist. Data types come in two families: primitives, which map to single JSON values, and complex types, which are small reusable structures.
| Family | Type | Example | Notes |
|---|---|---|---|
| Primitive | string, boolean, integer, decimal | "Jane", true, 42, 72.5 | decimal preserves precision - 1.50 and 1.5 are different values |
| Primitive | date, dateTime, instant, time | "1978-04-15", "2026-09-21T08:30:00-06:00" | date and dateTime may be partial ("1978", "1978-04"); if a time is present the time zone is mandatory |
| Primitive | code, uri, id, canonical | "final", "http://loinc.org" | code is a string drawn from a defined set; canonical points at a profile, value set or other definitional resource by its URL |
| Complex | HumanName, Address, ContactPoint | family/given/prefix; line/city/state/postalCode; system/value/use | The demographic building blocks |
| Complex | Identifier | system + value, e.g. an MRN in a named namespace | A business identifier - not the resource id (see below) |
| Complex | Coding, CodeableConcept | system + code + display; a list of Codings plus text | How every coded value is represented (covered under Terminology) |
| Complex | Quantity, Period, Range, Ratio | value + unit + system + code; start + end | Units use UCUM |
| Complex | Reference | { "reference": "Patient/123" } | The link between resources |
| Complex | Attachment, Annotation, Narrative | a PDF, a note, the XHTML summary |
Some elements can hold one of several types. The specification writes these as choice types with an [x] suffix - Observation.value[x], Patient.deceased[x] - and on the wire the type name is appended to the element name: valueQuantity, valueString, deceasedBoolean, deceasedDateTime. Only one may be present.
References
Resources are small on purpose, so a patient's record is a graph: an Observation refers to the Patient it is about (subject), the Encounter it was recorded in, the Practitioner who recorded it, and perhaps the Device that measured it. A Reference is usually a relative URL against the server's base - "Patient/123" - which is resolved by fetching [base]/Patient/123. It can also be an absolute URL to a resource on another server, a logical reference (an Identifier, when you know the MRN but not the server id), or a pointer to a contained resource: a resource embedded inside another ("#practitioner1") because it has no independent identity of its own. Contained resources are a code smell in most designs; use them for things that genuinely cannot stand alone.
Extensions
When the base resource does not have an element you need, you add an extension: a pair of a url that identifies the extension's definition and a value[x]. Extensions can appear on any element, including primitives, and there is a global registry of them (the US Core race and ethnicity extensions, the patient birth-place extension, and so on). A modifierExtension is the dangerous variety - it changes the meaning of the element it is attached to, and a receiver that does not understand it must not process the resource. The presence of extensions is not a sign of a bad implementation; it is how FHIR was designed to be used. Their absence from a profile's documentation is the thing to worry about.
A Real Resource, Element by Element
Here is a complete, valid R4 Patient resource, as a server would return it. It carries the same demographic facts as the PID segment in my HL7 v2 article, which is a useful comparison if you know v2.
{
"resourceType": "Patient",
"id": "123",
"meta": {
"versionId": "2",
"lastUpdated": "2026-09-21T08:30:00Z",
"profile": [ "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" ]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Jane A. Doe, female, born 1978-04-15. MRN123456.</div>"
},
"identifier": [ {
"use": "usual",
"type": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/v2-0203", "code": "MR" } ] },
"system": "http://hospital.example.org/identifiers/mrn",
"value": "MRN123456"
} ],
"active": true,
"name": [ { "use": "official", "family": "Doe", "given": [ "Jane", "A" ] } ],
"telecom": [
{ "system": "phone", "value": "(217) 555-0142", "use": "home" },
{ "system": "email", "value": "[email protected]" }
],
"gender": "female",
"birthDate": "1978-04-15",
"deceasedBoolean": false,
"address": [ {
"use": "home",
"line": [ "123 Main St" ],
"city": "Springfield", "state": "IL", "postalCode": "62701", "country": "USA"
} ],
"contact": [ {
"relationship": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/v2-0131", "code": "N" } ] } ],
"name": { "family": "Doe", "given": [ "John" ] },
"telecom": [ { "system": "phone", "value": "(217) 555-0143" } ]
} ],
"generalPractitioner": [ { "reference": "Practitioner/77", "display": "Dr. Robert Smith" } ],
"managingOrganization": { "reference": "Organization/genhosp" }
}
| Element | Value | What it means |
|---|---|---|
resourceType | Patient | The type. In JSON it is a property; in XML it is the root element name |
id | 123 | The server’s logical id. It is only meaningful on this server, and it is what appears in the URL [base]/Patient/123 |
meta.versionId | 2 | This is the second version of the resource. Combined with the id it addresses one exact version: Patient/123/_history/2 |
meta.profile | US Core Patient | The resource claims to conform to that profile. Claims are cheap; validation is what proves it |
text | XHTML | The narrative. US Core and many other profiles require it; the base spec does not |
identifier | MRN123456 in the hospital’s MRN namespace | A business identifier. The system URI makes the value unambiguous - “MRN123456” means nothing without knowing whose MRN it is. This is the v2 PID-3 and it is not the resource id |
name, telecom, address | Arrays of complex types. Note that given is itself an array - middle names are just further given names | |
gender | female | A code from the required administrative-gender value set (male, female, other, unknown). Clinical sex and gender identity are separate elements and extensions |
birthDate | 1978-04-15 | A date, so no time and no time zone. Could legally be "1978" if that is all you know |
deceasedBoolean | false | The choice type deceased[x], here as a boolean. It could instead be deceasedDateTime. Never both |
contact | John Doe, next of kin | A backbone element: a nested structure defined inline in Patient rather than a reusable type. The v2 NK1 segment |
generalPractitioner, managingOrganization | references | Links to a Practitioner and an Organization held on the same server. The optional display lets a client show a name without another round trip |
Now a clinical resource that refers to that patient. This Observation records a heart rate, and it shows the three things that make FHIR clinical data usable by machines: a coded statement of what was measured (LOINC), a quantity with a standard unit (UCUM), and a reference to who it is about.
{
"resourceType": "Observation",
"id": "hr-2026-09-21-0830",
"status": "final",
"category": [ { "coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "vital-signs" } ] } ],
"code": {
"coding": [ { "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" } ],
"text": "Heart rate"
},
"subject": { "reference": "Patient/123" },
"encounter": { "reference": "Encounter/enc-4471" },
"effectiveDateTime": "2026-09-21T08:25:00-06:00",
"performer": [ { "reference": "Practitioner/77" } ],
"valueQuantity": {
"value": 72,
"unit": "beats/minute",
"system": "http://unitsofmeasure.org",
"code": "/min"
}
}
status and code are the only two required elements in Observation, and status matters more than it looks: preliminary, final, amended, corrected and entered-in-error are how a result's life cycle is communicated, and a consuming system that ignores it will happily display a retracted lab value. The effectiveDateTime carries a time zone because it has a time component - the specification requires it, and it is the first thing that breaks when systems in different provinces or states exchange data.
For completeness, the same Patient begins like this in XML. The mapping is mechanical - resourceType becomes the root element, each element becomes a child with a value attribute, arrays become repeated elements - and any library will convert between the two. JSON is what nearly everyone uses; XML survives in some national programmes and wherever CDA people made the decision.
<Patient xmlns="http://hl7.org/fhir">
<id value="123"/>
<meta>
<versionId value="2"/>
<lastUpdated value="2026-09-21T08:30:00Z"/>
</meta>
<identifier>
<system value="http://hospital.example.org/identifiers/mrn"/>
<value value="MRN123456"/>
</identifier>
<name>
<family value="Doe"/>
<given value="Jane"/>
<given value="A"/>
</name>
<gender value="female"/>
<birthDate value="1978-04-15"/>
</Patient>
The RESTful API: One Interaction at a Time
A FHIR server exposes a base URL, and under it every resource type is a collection: [base]/Patient, [base]/Observation. Individual resources live at [base]/[type]/[id], specific versions at [base]/[type]/[id]/_history/[vid]. The interactions map onto HTTP verbs exactly as a web developer would expect, and the server tells you which ones it supports in its CapabilityStatement, available at GET [base]/metadata without authentication. That resource is FHIR's equivalent of a DICOM conformance statement, and reading it is the first step of every integration.
| Interaction | Request | Success | Notes |
|---|---|---|---|
| read | GET [base]/Patient/123 | 200 + resource | Returns ETag (the version) and Last-Modified |
| vread | GET [base]/Patient/123/_history/2 | 200 + that version | Old versions, if the server keeps history |
| create | POST [base]/Patient + resource | 201 + Location | Server assigns the id. If-None-Exist header makes it a conditional create |
| update | PUT [base]/Patient/123 + resource | 200 (or 201 if the server allows update-as-create) | Whole-resource replacement. Send If-Match with the ETag to avoid overwriting someone else’s change |
| patch | PATCH [base]/Patient/123 | 200 | JSON Patch, XML Patch or FHIRPath Patch - partial update |
| delete | DELETE [base]/Patient/123 | 204 (or 200 / 202) | Subsequent reads return 410 Gone, not 404; the history survives |
| history | GET [base]/Patient/123/_history | 200 + history Bundle | Also at type and system level |
| search | GET [base]/Patient?family=Doe&birthdate=1978-04-15 | 200 + searchset Bundle | Or POST [base]/Patient/_search with a form body, to keep parameters out of logs |
| capabilities | GET [base]/metadata | 200 + CapabilityStatement | What this server supports: resources, interactions, search parameters, operations, security |
| batch / transaction | POST [base] + Bundle | 200 + response Bundle | Several interactions in one request; a transaction is atomic |
| operation | GET/POST [base]/Patient/123/$everything | 200 | Named operations prefixed with $, defined by the spec or by an implementation guide |
Here is a create, exactly as it crosses the wire. The client sends the resource without an id; the server assigns one and reports where the resource now lives.
POST /fhir/Patient HTTP/1.1
Host: fhir.example.org
Content-Type: application/fhir+json
Accept: application/fhir+json
Authorization: Bearer eyJhbGciOi...
{ "resourceType": "Patient", "name": [ { "family": "Doe", "given": [ "Jane" ] } ], ... }
HTTP/1.1 201 Created
Location: https://fhir.example.org/fhir/Patient/123/_history/1
ETag: W/"1"
Last-Modified: Mon, 21 Sep 2026 14:30:00 GMT
Content-Type: application/fhir+json
Two details worth noticing. The media type is application/fhir+json, not plain application/json; strict servers reject the latter. And the ETag is a weak validator (W/"1") carrying the version id. An update that wants to be safe against lost updates sends it back:
PUT /fhir/Patient/123 HTTP/1.1
If-Match: W/"1"
Content-Type: application/fhir+json
{ "resourceType": "Patient", "id": "123", ... }
HTTP/1.1 200 OK
ETag: W/"2"
If someone else updated the patient in the meantime, the server answers 412 Precondition Failed and the client must re-read and retry. When anything goes wrong - a 400, 404, 412, 422 - the body is an OperationOutcome, FHIR's structured error. It is the equivalent of the ERR segment in a v2 NACK, and like the ERR segment it is only as useful as the server that produces it:
{
"resourceType": "OperationOutcome",
"issue": [ {
"severity": "error",
"code": "required",
"details": { "text": "Patient.identifier: minimum required = 1, but only found 0 (US Core Patient)" },
"expression": [ "Patient.identifier" ]
} ]
}
Search
Search is where FHIR stops being trivial. Each resource type defines a set of named search parameters (Patient has family, given, birthdate, identifier...; Observation has code, date, patient, value-quantity...), and each parameter has a type - string, token, date, number, quantity, reference, uri, composite - that determines what syntax it accepts. A realistic query, "the last twenty heart-rate readings for patient 123 this year, newest first, with the patient resource included", looks like this:
GET /fhir/Observation?patient=123
&code=http://loinc.org|8867-4
&date=ge2026-01-01
&_sort=-date
&_count=20
&_include=Observation:subject
| Feature | Syntax | What it does |
|---|---|---|
| Token search | code=http://loinc.org|8867-4 | Match a code in a system. code=8867-4 alone matches that code in any system |
| Prefixes on dates, numbers, quantities | date=ge2026-01-01, value-quantity=gt100 | eq ne gt lt ge le sa eb ap |
| Modifiers | family:exact=Doe, name:contains=oe, gender:missing=true | Change how a parameter matches |
| Chaining | Observation?subject.name=Doe | Search on a property of the referenced resource |
| Reverse chaining | Patient?_has:Observation:patient:code=8867-4 | Patients that have an observation with this code |
| Include / reverse include | _include=Observation:subject, _revinclude=Observation:patient | Return referenced / referencing resources in the same Bundle |
| Result control | _sort=-date, _count=20, _summary=true, _elements=code,value, _total=accurate | Ordering, page size, projections, counts |
The result is always a Bundle of type searchset. Its link array carries the paging URLs - self, next, previous - and the correct way to page is to follow next exactly as given, never to construct page URLs yourself. Each entry has a search.mode of match or include so you can tell the results from the resources that came along for the ride.
{
"resourceType": "Bundle",
"type": "searchset",
"total": 47,
"link": [
{ "relation": "self", "url": "https://fhir.example.org/fhir/Observation?patient=123&code=http%3A%2F%2Floinc.org%7C8867-4&date=ge2026-01-01&_sort=-date&_count=20&_include=Observation%3Asubject" },
{ "relation": "next", "url": "https://fhir.example.org/fhir?_getpages=b2c1a9e0&_getpagesoffset=20&_count=20" }
],
"entry": [
{ "fullUrl": "https://fhir.example.org/fhir/Observation/hr-2026-09-21-0830",
"resource": { "resourceType": "Observation", "id": "hr-2026-09-21-0830", "...": "..." },
"search": { "mode": "match" } },
{ "fullUrl": "https://fhir.example.org/fhir/Patient/123",
"resource": { "resourceType": "Patient", "id": "123", "...": "..." },
"search": { "mode": "include" } }
]
}
Two warnings that will save you a support ticket. Servers are only required to support the search parameters they list in their CapabilityStatement, and a parameter the server does not understand is - by default - silently ignored, not rejected, so a typo returns every observation on the server. Send Prefer: handling=strict if you want an error instead. And total is optional; do not build a UI that depends on it.
Bundles: Moving More Than One Resource
A Bundle is a resource that contains other resources. You have already met one - the searchset - but the same container serves every situation where resources travel together, and its type tells the receiver what the collection means and what to do with it.
| Bundle type | Used for | What the receiver does |
|---|---|---|
searchset | Search results | Reads entry and follows link[next] |
history | The result of a history interaction | Same shape as searchset, plus each entry’s request saying what happened |
transaction / transaction-response | Several creates, updates and deletes that must all succeed or all fail | Processes atomically, resolves temporary references between entries |
batch / batch-response | Several independent interactions in one round trip | Processes each; failures do not affect the others |
document | A clinical document - the first entry is a Composition that lists the rest | Stores or renders the document as a whole; the FHIR successor to CDA |
message | An event notification - the first entry is a MessageHeader | Acts on the event; the FHIR successor to a v2 message |
collection | Any set of resources with no other semantics | Whatever it likes |
subscription-notification (R5) | Delivered when a subscription fires | Handles the event |
The transaction is the one that repays study, because it solves the problem every integration hits on day one: how to create a Patient and an Observation about that Patient in one atomic step, when you do not yet know the id the server will give the Patient. The answer is a temporary urn:uuid: in fullUrl; the server rewrites every reference to it once it has assigned real ids.
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:7d5b8c3e-1f2a-4b6c-9d0e-1a2b3c4d5e6f",
"resource": { "resourceType": "Patient", "name": [ { "family": "Doe", "given": [ "Jane" ] } ] },
"request": { "method": "POST", "url": "Patient",
"ifNoneExist": "identifier=http://hospital.example.org/identifiers/mrn|MRN123456" }
},
{
"resource": {
"resourceType": "Observation",
"status": "final",
"code": { "coding": [ { "system": "http://loinc.org", "code": "8867-4" } ] },
"subject": { "reference": "urn:uuid:7d5b8c3e-1f2a-4b6c-9d0e-1a2b3c4d5e6f" },
"valueQuantity": { "value": 72, "system": "http://unitsofmeasure.org", "code": "/min" }
},
"request": { "method": "POST", "url": "Observation" }
}
]
}
The ifNoneExist on the first entry turns it into a conditional create - "create this patient unless one with this MRN already exists, and in that case use the existing one" - which is how you make the transaction safe to replay. The response is a transaction-response Bundle with one entry per request, each carrying a response.status like "201 Created" and the location of the new resource.
Terminology: Codes, Code Systems and Value Sets
Structured data is worthless if two systems use different words for the same thing, so FHIR is unusually careful about coded values. I covered the vocabularies themselves - SNOMED CT, LOINC, ICD, RxNorm and friends - in a separate article; here is how FHIR carries them.
- A Coding is one code from one system:
{ "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" }. Thesystemis a URI that identifies the code system unambiguously; thedisplayis for humans and must not be used for matching. - A CodeableConcept is a list of Codings plus optional free text - because the same concept is often expressed in several systems at once (a LOINC code and a local lab code), and because sometimes there is only text. Nearly every clinical coded element is a CodeableConcept.
- A CodeSystem resource defines a set of codes and their meanings. A ValueSet resource selects codes from one or more code systems for a particular purpose ("the codes allowed in
Observation.status", "all SNOMED CT descendants of clinical finding"). A ConceptMap translates between value sets. - Each coded element is bound to a value set with a binding strength: required (must be from this set), extensible (must be from this set if it has a suitable code), preferred or example. Profiles routinely tighten bindings - US Core binds
Observation.codefor vital signs to specific LOINC codes. - A terminology server (many FHIR servers include one; tx.fhir.org is the reference) answers the questions you cannot answer locally through operations:
ValueSet/$expand,ValueSet/$validate-code,CodeSystem/$lookup,ConceptMap/$translate.
Profiles and Implementation Guides: Where “FHIR Compliant” Gets Its Meaning
Because the base specification makes nearly everything optional, two systems can both be perfectly conformant to FHIR R4 and still be unable to exchange a patient - one requires an MRN and the other never sends one. FHIR anticipated this. The base resources are meant to be profiled: a profile is a StructureDefinition resource that takes a base resource and constrains it for a purpose. A profile can:
- tighten cardinality - make
Patient.identifierrequired, forbidPatient.photo; - fix values or bind an element to a particular value set with a stronger binding;
- slice a repeating element so that, say, one
identifiermust be an MRN and another a health card number, each with its own rules; - add extensions and say which are required;
- flag elements as Must Support - the implementation guide defines what that means, but typically "a sender must populate it if it has the data and a receiver must not fail on it";
- attach invariants, written in FHIRPath, that a validator will check (
name.exists() or identifier.exists()).
Profiles rarely travel alone. They are published in implementation guides (IGs) - websites generated from a package of profiles, extensions, value sets, examples, search parameter definitions and, crucially, prose that says how the whole thing is meant to be used. When someone says "we support FHIR", the question that means something is "which IGs, which version?". The ones you will keep meeting:
| Implementation guide | Publisher | What it profiles |
|---|---|---|
| US Core | HL7 US Realm | The US baseline: the data classes of USCDI as R4 profiles. Mandated by ONC certification; the reference point for nearly every US API |
| International Patient Summary (IPS) | HL7 International, with ISO | A minimal, cross-border patient summary document. Adopted by the EU (EHDS), the WHO’s Global Digital Health Certification Network and others |
| CA Core / pan-Canadian profiles | Canada Health Infoway | Canadian baseline profiles; see my Java and .NET articles |
| UK Core, AU Core, ISiK, KBV, … | National programmes | Each country’s baseline. They share the same shape and differ in identifiers, terminologies and extensions |
| SMART App Launch | HL7 | Not profiles of data but of security: how an app authorises against an EHR (below) |
| Bulk Data Access | HL7 | The $export operation for population-scale extraction |
| IHE MHD, PIXm, PDQm, mCSD, … | IHE International | Document sharing, patient identity cross-referencing, demographics query and directories over FHIR; the FHIR siblings of XDS, PIX and PDQ that I describe in my IHE article |
| Da Vinci (CRD, DTR, PAS, PDex, …) | HL7 / payers | Payer-provider exchange: prior authorisation, coverage, payer data - the basis of the CMS rules |
| mCODE, CARIN Blue Button, Gravity, … | Domain accelerators | Oncology, consumer claims, social determinants |
The tooling around IGs is mature. Profiles are usually written in FHIR Shorthand (FSH), a compact text language compiled by SUSHI; the IG Publisher turns the package into the website; the official FHIR validator (and any server that implements $validate) checks a resource against a profile; registry.fhir.org and Simplifier host published packages. My Java and .NET validation articles show the validator in code.
Security and the Wider Ecosystem
The FHIR specification deliberately does not define authentication or authorisation; it says "use TLS, use OAuth 2.0, and here are the resources for audit and consent" and leaves the rest to implementation guides. In practice one guide won: SMART App Launch ("SMART on FHIR"). It profiles OAuth 2.0 and OpenID Connect for healthcare, defining how an app discovers the authorisation server ([base]/.well-known/smart-configuration), how it is launched from inside an EHR with the current patient in context (EHR launch) or on its own (standalone launch), and a scope syntax that maps onto resources - patient/Observation.rs means "read and search Observations for the patient in context", user/*.cruds means "everything the logged-in user may do". A companion Backend Services profile covers system-to-system access with a signed JWT and the client-credentials grant, and it is what the Bulk Data API uses. I walk through building a SMART app in Java and .NET.
Beyond the core API, a few specifications have become part of what "FHIR" means in practice:
- Bulk Data Access -
$exportat the system, Group or Patient level, returning newline-delimited JSON files asynchronously (Prefer: respond-async, a 202, a status URL to poll). This is how population health, analytics and payer exchanges get data out without a million individual GETs. - Subscriptions - "tell me when a new Observation appears for this patient". R4 has a simple criteria-based Subscription resource with REST-hook, WebSocket and email channels; R5 replaced it with a topic-based design (
SubscriptionTopic,Subscription,SubscriptionStatus) that was also back-ported to R4 as an IG. Support varies widely between servers. - CDS Hooks - a companion standard for clinical decision support: the EHR calls a service at defined moments (patient-view, order-sign) with FHIR context and gets back "cards" to display. Not FHIR REST, but built on FHIR resources.
- Operations beyond CRUD -
Patient/$everything,$validate,$matchfor patient matching,$documentto assemble a document Bundle,Measure/$evaluate-measurefor quality reporting. The CapabilityStatement lists which ones a server implements. - AuditEvent, Provenance and Consent - the resources that answer "who did what, where did this come from, and was it allowed". AuditEvent shares its model with the DICOM audit message that IHE's ATNA profile uses.
Finally, the servers and tools. You do not have to build a FHIR server - almost nobody does - and the choice is wide enough that you should expect to meet several.
| Category | Examples | Notes |
|---|---|---|
| Open-source servers and SDKs | HAPI FHIR (Java), Firely SDK and Firely Server (.NET), Medplum (TypeScript), IBM/LinuxForHealth FHIR (Java), Aidbox | HAPI and Firely are the two reference libraries; my Java and .NET series use them |
| Cloud managed services | Azure Health Data Services, Google Cloud Healthcare API, AWS HealthLake, Oracle Health | R4 (and often R5) servers as a managed service, with bulk export and analytics hooks |
| EHR vendor APIs | Epic, Oracle Health (Cerner), MEDITECH, athenahealth and every other certified US EHR; regional systems in other countries | Each publishes a developer portal, a sandbox and a CapabilityStatement. Expect US Core R4 in the US and the national baseline elsewhere |
| Public test servers | hapi.fhir.org, server.fire.ly | Anyone can write to them, so never put real data there, and expect other people’s junk in your search results |
| Validation and testing | The official validator, Inferno (ONC certification tests), Touchstone, Synthea for synthetic patients | Inferno’s test kits are the quickest way to find out what “US Core conformant” really demands |
| Community | chat.fhir.org, HL7 Connectathons (three a year), confluence.hl7.org | The Zulip chat is where the specification’s authors answer questions, often within the hour |
The Parts Nobody Warns You About
Everything above is in the specification. What follows is what you learn on the second week of a FHIR project.
"FHIR compliant" says almost nothing
Exactly as with "HL7 compliant" and "IHE compliant", the claim needs three qualifiers before it means anything: which version (R4? R5?), which profiles (US Core 6.1? CA Core? the vendor's own?), and which interactions and search parameters (read the CapabilityStatement - it is common for a vendor API to support read and search on a dozen resources and nothing else, with search parameters limited to a handful per resource). Ask for the CapabilityStatement and the IG conformance claims in the RFP, and run Inferno or the validator against the sandbox before signing.
id is not identifier
This is the mistake every newcomer makes once. id is the server's key; it is assigned by the server, it is only meaningful on that server, and it can be different for the same patient on two servers. identifier is the real-world identifier - MRN, health card, insurance number - and it is what you use to find a patient you know from elsewhere: GET /Patient?identifier=http://hospital.example.org/identifiers/mrn|MRN123456. Never store a FHIR id from someone else's server as if it were a business key, and never expect your id to be preserved when you POST to another server.
Versions break things quietly
R4 and R5 are different specifications. Some changes are loud (renamed resources, MedicationRequest.medication changing from a choice type to CodeableReference); some are quiet (a value set gained or lost codes, a search parameter changed type). The libraries handle this by shipping a separate model per version - HAPI's org.hl7.fhir.r4.model versus r5.model, Firely's separate NuGet packages - and you will pick one at project start. Cross-version conversion exists but is lossy. Pin the version, put it in the media type if the server supports it (application/fhir+json; fhirVersion=4.0), and treat "we will just upgrade to R5 later" as the multi-month project it is.
JSON is FHIR JSON, not just JSON
The JSON representation has rules a generic parser will not enforce. Choice types are named with their type (valueQuantity, never value). Primitives can carry extensions and ids through a shadow property with an underscore prefix: a birthDate with a data-absent-reason extension is written as "_birthDate": { "extension": [...] } next to the value. null is not allowed anywhere except as a placeholder in arrays of primitives. Empty strings, empty arrays and empty objects are all forbidden - omit the element instead. Decimals must preserve their precision, which many JSON libraries silently destroy by parsing into a double. Use a FHIR library for serialisation; hand-rolling it works until the first real-world resource.
Search is not SQL
Search is a defined set of parameters with defined semantics, not an arbitrary query language. You cannot search on an element unless a search parameter exists for it (or you define a custom SearchParameter and the server supports it). String searches are case- and accent-insensitive prefix matches by default. Token searches on codes require the system for precision. Servers may cap _count, may ignore _sort on parameters they did not index, may refuse chains more than one level deep, and are allowed to return total as an estimate or not at all. Anything analytical - "average heart rate by age band" - belongs in a bulk export and a database, not in search.
References stop at the server boundary
"reference": "Practitioner/77" resolves on the server that produced it and nowhere else. The moment resources are copied to another server, moved through an integration engine, or assembled from several sources, relative references break unless somebody rewrites them (transaction Bundles do this for you; ad hoc copying does not). Aggregating data from several FHIR servers means keeping track of which base URL each resource came from and normalising identity through identifier, not id. IHE's PIXm profile exists for precisely this.
Dates, times and precision
A date may be a year, a year-month or a full date, and a dateTime may be any of those or a full timestamp - and if it has a time, it must have a time zone. Systems that store everything as UTC timestamps lose the distinction between "born in 1978" and "born on 1 January 1978 at midnight", and systems that strip time zones produce results that are off by hours across a provincial border. Keep the string, keep the precision, and compare with care. Search on dates uses the precision of the parameter you send: date=2026-09 matches the whole month.
Narrative is required more often than you think
The base spec makes text optional; US Core, IPS and many other profiles make it mandatory for key resources, and a document Bundle relies on it for human-readable rendering. Generate it - most libraries can - but never parse it: the narrative is for humans and may legitimately say things the structured elements do not.
FHIR, HL7 v2, CDA and DICOM: Where Each One Lives
Engineers arriving from web development sometimes assume FHIR has replaced everything else, and engineers arriving from hospital integration sometimes assume it is a passing fashion. Neither is true. The standards sit in different places, and FHIR's real role in 2026 is to be the face the rest of them present to the outside world.
| HL7 v2 | HL7 CDA (v3) | HL7 FHIR | DICOM | |
|---|---|---|---|---|
| What it is | Event-driven text messages | XML clinical documents | REST API + resources (also documents and messages) | Image objects, file format and network services |
| Unit of exchange | A message (ADT^A01, ORU^R01…) | A document (CCD, discharge summary) | A resource, or a Bundle of them | An instance (image, SR, segmentation) in a study |
| Transport | MLLP over TCP, via an integration engine | XDS, Direct, MLLP, email | HTTP(S) with OAuth 2.0 | DIMSE over TCP; DICOMweb over HTTP |
| Format | Delimited text | XML with a fixed schema and templates | JSON or XML | Binary tag-value encoding; JSON/XML for metadata via DICOMweb |
| Identity of things | Identifiers in fields (PID-3) | Identifiers in the document | Server ids plus business identifiers | UIDs, globally unique |
| Strength | Ubiquitous inside the hospital; cheap; every system speaks it | Legally durable, human-readable, signed documents | Developer-friendly, granular, queryable, app-ready | The only way medical images move; decades of conformance |
| Where you meet it in 2026 | ADT, orders and results feeds between hospital systems | Care summaries, transitions of care, national document exchanges, regulated document submissions | Patient-facing apps, EHR APIs, payer exchange, national programmes, cloud analytics, anything new | Every scanner, PACS, viewer and imaging archive |
They connect at well-defined seams. The HL7 v2-to-FHIR implementation guide maps segments to resources (PID to Patient, PV1 to Encounter, OBX to Observation), and integration engines routinely convert an inbound v2 feed into FHIR resources for an API. A CDA document can be represented as a FHIR document Bundle, and the IPS is now published in both forms. On the imaging side, FHIR's ImagingStudy resource describes a DICOM study and points at its images through DICOMweb endpoints, and DiagnosticReport carries the radiology report; I cover that seam in detail in DICOM and FHIR. And IHE profiles - MHD, PIXm, PDQm, the imaging profiles built on DICOMweb - are the choreography that says which of these to use together for a given workflow. A hospital in 2026 runs all four, and the interesting engineering happens where they meet.
Where to Go Next
This article is the map. From here, depending on what you are trying to do:
- Write code: the Java series starts with setting up HAPI FHIR and works through creating, reading, updating, searching, validating, transactions, security and a SMART app. The .NET series with the Firely SDK covers the same ground, starting here.
- Profiles in practice: the pan-Canadian profiles in Java and .NET show what building against a national IG actually involves.
- The longer treatment: my FHIR tutorial goes deeper on architecture, search, data types and extensions, security, validation and profiles and bulk data.
- The neighbours: HL7 v2, HL7 v3 and CDA, DICOM, DICOM and FHIR, IHE and coded vocabularies.
- The source: the R4 specification (start with the developer overview, then the RESTful API and search), the R5 specification, the SMART App Launch guide, and US Core as the canonical example of a profile set.
- Test yourself: the HL7 interactive quiz covers v2, v3, CDA and FHIR, and the health informatics standards quiz places FHIR among its neighbours.
Glossary
The terms above, in one place, with a link to where each is defined in R4.
| Term | Meaning |
|---|---|
| Resource | The unit of exchange: a typed, identifiable chunk of healthcare data (Patient, Observation…) |
| Element | A named, typed field within a resource, with a cardinality |
| Data type | Primitive (string, date, code…) or complex (HumanName, CodeableConcept, Quantity…) type of an element |
| id | The server-assigned logical id of a resource; part of its URL |
| Identifier | A business identifier (MRN, health card number) with a system and a value |
| Reference | A link from one resource to another, usually a relative URL |
| Contained resource | A resource embedded inside another because it has no independent identity |
| Extension | A url + value pair that adds an element the base resource lacks; a modifierExtension changes meaning |
| Narrative | The XHTML human-readable summary in text.div |
| Coding / CodeableConcept | One code from one system / a set of codings plus text for the same concept |
| CodeSystem / ValueSet / ConceptMap | Defines codes / selects codes for a purpose / translates between them |
| Binding strength | How strictly an element must use its value set: required, extensible, preferred, example |
| Bundle | A container resource: searchset, history, transaction, batch, document, message, collection |
| Interaction | One of the defined API actions: read, vread, create, update, patch, delete, history, search, capabilities |
| Operation | A named, $-prefixed action beyond CRUD, e.g. $everything, $validate, $export |
| Search parameter | A named, typed criterion a resource type can be searched by |
| OperationOutcome | The structured error / warning / information resource returned when something goes wrong |
| CapabilityStatement | The server’s conformance statement, at [base]/metadata |
| StructureDefinition | The resource that defines a resource type, a data type, an extension or a profile |
| Profile | A StructureDefinition that constrains a base resource for a purpose |
| Implementation guide | A published package of profiles, value sets, extensions, examples and prose for a use case or jurisdiction |
| Must Support | A profile flag whose exact meaning the IG defines; typically “send it if you have it, accept it if you get it” |
| FHIRPath | The path and expression language used in invariants, search parameter definitions and patches |
| Maturity level (FMM) | 0-5 then Normative; how stable a resource or page is within a release |
| SMART on FHIR | The OAuth 2.0 / OpenID Connect profile for authorising apps and services against a FHIR server |
| Messaging / Documents | The two non-REST exchange paradigms, both carried in Bundles |
If you work with healthcare software, or with standards such as HL7, FHIR or DICOM, and want to compare notes with other engineers doing the same, join the discussion in my LinkedIn group on healthcare interoperability engineering - questions, war stories and lessons learned welcome.