FHIR Explained: The API Standard Behind Modern Healthcare Data Exchange in 2026

| Updated on

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.

FHIR - Standing on the Web's Shoulders saravanansubramanian.com Implementation Guides / Profiles US Core, IPS, ISiK, IHE MHD, Canadian CA-Core - constrain resources for a jurisdiction or use case FHIR Resources Patient, Observation, Encounter, MedicationRequest, ... (~150 in R4) Serialization - JSON or XML deterministic mapping - same resource, two on-the-wire formats REST - Resource-oriented API style GET /Patient/123 - POST /Observation - PUT /Encounter/9 - DELETE /... - stateless HTTP + TLS - the transport standard verbs, standard headers, standard status codes - familiar to every web developer FHIR did not invent REST or JSON. It stopped forcing healthcare devs to invent their own transport for every integration. The lower the layer, the more universal - so newcomers spend their learning budget on resources and profiles, not plumbing.

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.

ReleasePublishedStatusWhere you meet it
DSTU1 (0.0.82)Feb 2014RetiredNowhere today; historical interest only
DSTU2 (1.0.2)Oct 2015RetiredThe 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 2017RetiredSome national programmes and older vendor APIs; migrating to R4
R4 (4.0.1)Dec 2018 / Oct 2019Normative coreThe 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 2022Trial UseA bridge release that back-ported a few R5 items (medication definitions, subscription topics) onto R4. Rarely targeted directly
R5 (5.0.0)Mar 2023Trial UseNew 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 ballotNormative ballotHL7 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.

FHIR Releases - and the One Everyone Actually Runs saravanansubramanian.com 2011 Resources for Health proposal DSTU1 2014 DSTU2 2015 Argonaut, first EHR APIs, SMART STU3 2017 R4 4.0.1 - 2019 normative core US Cures Act, US Core CA Core, UK Core, IPS R4B 2022 R5 2023 new subscriptions, breaking changes R6 in ballot 2026-27 Versions are not wire-compatible. Ask "which version?" before anything else - and expect the answer to be R4 for years yet. Regulation froze R4 in place the same way hospital installs froze HL7 v2.5.1 in place.

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.

FamilyTypeExampleNotes
Primitivestring, boolean, integer, decimal"Jane", true, 42, 72.5decimal preserves precision - 1.50 and 1.5 are different values
Primitivedate, 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
Primitivecode, 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
ComplexHumanName, Address, ContactPointfamily/given/prefix; line/city/state/postalCode; system/value/useThe demographic building blocks
ComplexIdentifiersystem + value, e.g. an MRN in a named namespaceA business identifier - not the resource id (see below)
ComplexCoding, CodeableConceptsystem + code + display; a list of Codings plus textHow every coded value is represented (covered under Terminology)
ComplexQuantity, Period, Range, Ratiovalue + unit + system + code; start + endUnits use UCUM
ComplexReference{ "reference": "Patient/123" }The link between resources
ComplexAttachment, Annotation, Narrativea 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.

Anatomy of a Resource saravanansubramanian.com { "resourceType": "Patient", "id": "123", "meta": { "versionId": "2", "lastUpdated": "2026-09-21T08:30:00Z", "profile": [ ".../us-core-patient" ] }, "text": { "status": "generated", "div": "<div>Jane A. Doe, F, 1978</div>" }, "identifier": [ { "system": ".../mrn", "value": "MRN123456" } ], "extension": [ { "url": ".../birthPlace", "valueAddress": {...} } ], "name": [ { "family": "Doe", "given": [ "Jane", "A" ] } ], "gender": "female", "birthDate": "1978-04-15", "generalPractitioner": [ { "reference": "Practitioner/77" } ] } Type one of ~150 - decides which elements are allowed Logical id assigned by the server; appears in the URL Metadata version, timestamp, claimed profiles Narrative XHTML a human can read when the codes fail Business identifier(s) MRN, health card number - NOT the id Extension url + value[x] for the 20% the base omits Typed elements HumanName, code, date... each with cardinality Reference a link to another resource - the record is a graph Every resource type shares the first four parts. Only the typed elements differ.

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" }
}
ElementValueWhat it means
resourceTypePatientThe type. In JSON it is a property; in XML it is the root element name
id123The server’s logical id. It is only meaningful on this server, and it is what appears in the URL [base]/Patient/123
meta.versionId2This is the second version of the resource. Combined with the id it addresses one exact version: Patient/123/_history/2
meta.profileUS Core PatientThe resource claims to conform to that profile. Claims are cheap; validation is what proves it
textXHTMLThe narrative. US Core and many other profiles require it; the base spec does not
identifierMRN123456 in the hospital’s MRN namespaceA 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, addressArrays of complex types. Note that given is itself an array - middle names are just further given names
genderfemaleA code from the required administrative-gender value set (male, female, other, unknown). Clinical sex and gender identity are separate elements and extensions
birthDate1978-04-15A date, so no time and no time zone. Could legally be "1978" if that is all you know
deceasedBooleanfalseThe choice type deceased[x], here as a boolean. It could instead be deceasedDateTime. Never both
contactJohn Doe, next of kinA backbone element: a nested structure defined inline in Patient rather than a reusable type. The v2 NK1 segment
generalPractitioner, managingOrganizationreferencesLinks 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.

InteractionRequestSuccessNotes
readGET [base]/Patient/123200 + resourceReturns ETag (the version) and Last-Modified
vreadGET [base]/Patient/123/_history/2200 + that versionOld versions, if the server keeps history
createPOST [base]/Patient + resource201 + LocationServer assigns the id. If-None-Exist header makes it a conditional create
updatePUT [base]/Patient/123 + resource200 (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
patchPATCH [base]/Patient/123200JSON Patch, XML Patch or FHIRPath Patch - partial update
deleteDELETE [base]/Patient/123204 (or 200 / 202)Subsequent reads return 410 Gone, not 404; the history survives
historyGET [base]/Patient/123/_history200 + history BundleAlso at type and system level
searchGET [base]/Patient?family=Doe&birthdate=1978-04-15200 + searchset BundleOr POST [base]/Patient/_search with a form body, to keep parameters out of logs
capabilitiesGET [base]/metadata200 + CapabilityStatementWhat this server supports: resources, interactions, search parameters, operations, security
batch / transactionPOST [base] + Bundle200 + response BundleSeveral interactions in one request; a transaction is atomic
operationGET/POST [base]/Patient/123/$everything200Named 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
FeatureSyntaxWhat it does
Token searchcode=http://loinc.org|8867-4Match a code in a system. code=8867-4 alone matches that code in any system
Prefixes on dates, numbers, quantitiesdate=ge2026-01-01, value-quantity=gt100eq ne gt lt ge le sa eb ap
Modifiersfamily:exact=Doe, name:contains=oe, gender:missing=trueChange how a parameter matches
ChainingObservation?subject.name=DoeSearch on a property of the referenced resource
Reverse chainingPatient?_has:Observation:patient:code=8867-4Patients that have an observation with this code
Include / reverse include_include=Observation:subject, _revinclude=Observation:patientReturn referenced / referencing resources in the same Bundle
Result control_sort=-date, _count=20, _summary=true, _elements=code,value, _total=accurateOrdering, 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.

One Client, One Server, One Patient saravanansubramanian.com Amber arrows are requests. Grey arrows are responses. Every response body is a resource - or an OperationOutcome. FHIR Client app, engine, portal FHIR Server EHR, HAPI, Firely, cloud GET /metadata 200 OK - CapabilityStatement: what this server supports POST /Patient { no id } 201 Created - Location: /Patient/123/_history/1 - ETag: W/"1" GET /Patient/123 200 OK - the Patient resource - ETag: W/"1" PUT /Patient/123 If-Match: W/"1" 200 OK - ETag: W/"2" (stale If-Match: 412 Precondition Failed + OperationOutcome) GET /Observation?patient=123&code=8867-4&_sort=-date 200 OK - Bundle (searchset) with self / next links DELETE /Patient/123 204 No Content (a later GET returns 410 Gone - history is kept)

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 typeUsed forWhat the receiver does
searchsetSearch resultsReads entry and follows link[next]
historyThe result of a history interactionSame shape as searchset, plus each entry’s request saying what happened
transaction / transaction-responseSeveral creates, updates and deletes that must all succeed or all failProcesses atomically, resolves temporary references between entries
batch / batch-responseSeveral independent interactions in one round tripProcesses each; failures do not affect the others
documentA clinical document - the first entry is a Composition that lists the restStores or renders the document as a whole; the FHIR successor to CDA
messageAn event notification - the first entry is a MessageHeaderActs on the event; the FHIR successor to a v2 message
collectionAny set of resources with no other semanticsWhatever it likes
subscription-notification (R5)Delivered when a subscription firesHandles 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" }. The system is a URI that identifies the code system unambiguously; the display is 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.code for 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.identifier required, forbid Patient.photo;
  • fix values or bind an element to a particular value set with a stronger binding;
  • slice a repeating element so that, say, one identifier must 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 guidePublisherWhat it profiles
US CoreHL7 US RealmThe 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 ISOA 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 profilesCanada Health InfowayCanadian baseline profiles; see my Java and .NET articles
UK Core, AU Core, ISiK, KBV, …National programmesEach country’s baseline. They share the same shape and differ in identifiers, terminologies and extensions
SMART App LaunchHL7Not profiles of data but of security: how an app authorises against an EHR (below)
Bulk Data AccessHL7The $export operation for population-scale extraction
IHE MHD, PIXm, PDQm, mCSD, …IHE InternationalDocument 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 / payersPayer-provider exchange: prior authorisation, coverage, payer data - the basis of the CMS rules
mCODE, CARIN Blue Button, Gravity, …Domain acceleratorsOncology, 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.

From Base Resource to Something You Can Build Against saravanansubramanian.com Base resource - Patient (FHIR R4 specification) ~25 elements, all 0..1 or 0..* - no required identifier, no required name - "valid FHIR" but two conformant systems may still not interoperate constrain Profile - a StructureDefinition (e.g. US Core Patient, CA Core Patient) identifier 1..* - name 1..* - gender bound (required) to administrative-gender - Must Support flags slices: identifier[MRN], identifier[healthCardNumber] - extensions: race, ethnicity, birthsex - invariants in FHIRPath the resource now says which of the base's options this community has agreed on package Implementation Guide - versioned, published profiles + extensions + value sets + search parameters + operations + examples + capability statement + the prose: who sends what, when, and what "Must Support" means here - written in FSH, built by the IG Publisher implement + validate Your system: "conforms to US Core 6.1.0 Patient" a claim a validator can check - and the only claim worth putting in an RFP "FHIR compliant" names the top box. Real interoperability lives in the bottom one.

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 - $export at 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, $match for patient matching, $document to assemble a document Bundle, Measure/$evaluate-measure for 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.

CategoryExamplesNotes
Open-source servers and SDKsHAPI FHIR (Java), Firely SDK and Firely Server (.NET), Medplum (TypeScript), IBM/LinuxForHealth FHIR (Java), AidboxHAPI and Firely are the two reference libraries; my Java and .NET series use them
Cloud managed servicesAzure Health Data Services, Google Cloud Healthcare API, AWS HealthLake, Oracle HealthR4 (and often R5) servers as a managed service, with bulk export and analytics hooks
EHR vendor APIsEpic, Oracle Health (Cerner), MEDITECH, athenahealth and every other certified US EHR; regional systems in other countriesEach publishes a developer portal, a sandbox and a CapabilityStatement. Expect US Core R4 in the US and the national baseline elsewhere
Public test servershapi.fhir.org, server.fire.lyAnyone can write to them, so never put real data there, and expect other people’s junk in your search results
Validation and testingThe official validator, Inferno (ONC certification tests), Touchstone, Synthea for synthetic patientsInferno’s test kits are the quickest way to find out what “US Core conformant” really demands
Communitychat.fhir.org, HL7 Connectathons (three a year), confluence.hl7.orgThe 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 v2HL7 CDA (v3)HL7 FHIRDICOM
What it isEvent-driven text messagesXML clinical documentsREST API + resources (also documents and messages)Image objects, file format and network services
Unit of exchangeA message (ADT^A01, ORU^R01…)A document (CCD, discharge summary)A resource, or a Bundle of themAn instance (image, SR, segmentation) in a study
TransportMLLP over TCP, via an integration engineXDS, Direct, MLLP, emailHTTP(S) with OAuth 2.0DIMSE over TCP; DICOMweb over HTTP
FormatDelimited textXML with a fixed schema and templatesJSON or XMLBinary tag-value encoding; JSON/XML for metadata via DICOMweb
Identity of thingsIdentifiers in fields (PID-3)Identifiers in the documentServer ids plus business identifiersUIDs, globally unique
StrengthUbiquitous inside the hospital; cheap; every system speaks itLegally durable, human-readable, signed documentsDeveloper-friendly, granular, queryable, app-readyThe only way medical images move; decades of conformance
Where you meet it in 2026ADT, orders and results feeds between hospital systemsCare summaries, transitions of care, national document exchanges, regulated document submissionsPatient-facing apps, EHR APIs, payer exchange, national programmes, cloud analytics, anything newEvery 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:

Glossary

The terms above, in one place, with a link to where each is defined in R4.

TermMeaning
ResourceThe unit of exchange: a typed, identifiable chunk of healthcare data (Patient, Observation…)
ElementA named, typed field within a resource, with a cardinality
Data typePrimitive (string, date, code…) or complex (HumanName, CodeableConcept, Quantity…) type of an element
idThe server-assigned logical id of a resource; part of its URL
IdentifierA business identifier (MRN, health card number) with a system and a value
ReferenceA link from one resource to another, usually a relative URL
Contained resourceA resource embedded inside another because it has no independent identity
ExtensionA url + value pair that adds an element the base resource lacks; a modifierExtension changes meaning
NarrativeThe XHTML human-readable summary in text.div
Coding / CodeableConceptOne code from one system / a set of codings plus text for the same concept
CodeSystem / ValueSet / ConceptMapDefines codes / selects codes for a purpose / translates between them
Binding strengthHow strictly an element must use its value set: required, extensible, preferred, example
BundleA container resource: searchset, history, transaction, batch, document, message, collection
InteractionOne of the defined API actions: read, vread, create, update, patch, delete, history, search, capabilities
OperationA named, $-prefixed action beyond CRUD, e.g. $everything, $validate, $export
Search parameterA named, typed criterion a resource type can be searched by
OperationOutcomeThe structured error / warning / information resource returned when something goes wrong
CapabilityStatementThe server’s conformance statement, at [base]/metadata
StructureDefinitionThe resource that defines a resource type, a data type, an extension or a profile
ProfileA StructureDefinition that constrains a base resource for a purpose
Implementation guideA published package of profiles, value sets, extensions, examples and prose for a use case or jurisdiction
Must SupportA profile flag whose exact meaning the IG defines; typically “send it if you have it, accept it if you get it”
FHIRPathThe 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 FHIRThe OAuth 2.0 / OpenID Connect profile for authorising apps and services against a FHIR server
Messaging / DocumentsThe 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.