Skip to content
Naveen Raj

FHIR, in practice

9/11/2026 · 7 min read · 160 views

FHIRHealthcarebackendHL7

Every FHIR talk starts the same way: a slide with "Patient," "Observation," and "Encounter" in boxes, arrows between them, and a promise that healthcare data interoperability is now a solved problem. It isn't wrong, exactly. It's just missing the six months of decisions that come after you've read the spec and before anything in production actually works.

I've spent the last year and a half building the backend for a healthcare platform on FHIR R4 — patient records, scheduling, clinical documentation, terminology, the works. This is what I wish someone had told me before I started, instead of what the spec tells you.

The resource model is the easy part

FHIR gives you resources — Patient, Practitioner, Organization, Encounter, Observation, Condition, DiagnosticReport, MedicationRequest, Procedure, Schedule, Slot, Appointment — each one a structured JSON document with a defined shape. Reading and writing them is genuinely pleasant. Here's a fairly ordinary Observation, the kind you'd get back from a vitals check:

{
  "resourceType": "Observation",
  "status": "final",
  "code": {
    "coding": [
      {
        "system": "http://loinc.org",
        "code": "8867-4",
        "display": "Heart rate"
      }
    ]
  },
  "subject": { "reference": "Patient/e3f1c9" },
  "encounter": { "reference": "Encounter/a02d47" },
  "effectiveDateTime": "2026-03-14T09:12:00+05:30",
  "valueQuantity": {
    "value": 78,
    "unit": "beats/minute",
    "system": "http://unitsofmeasure.org",
    "code": "/min"
  }
}

Nothing here is hard. subject and encounter are references to other resources, code identifies what was measured using an external coding system, valueQuantity is the measurement itself. If you've built a REST API before, this looks like a REST API. The problem is that this comfort is exactly what gets teams into trouble — the resource model is deliberately generic, and generic means every implementer has to make the same hundred decisions independently, with no feedback from the spec on whether they made the right ones.

References vs. contained resources

FHIR gives you two ways to relate resources: reference them by ID ({"reference": "Patient/e3f1c9"}) or nest them inline as contained resources when they don't deserve to exist independently. The spec is honest that contained is a last resort, and in practice we ended up banning it outright in our profile. It seems convenient — no extra round trip, no orphaned records — until you need to search across that nested data, or that "throwaway" resource turns out to matter later (a contained Practitioner that a compliance audit suddenly needs to query by name, say). Every resource gets a real ID and a real reference. The extra round trips are a non-issue once you have a decent data loader in front of the API; the alternative — data you structurally can't query — is not something you can retrofit.

Terminology is where the real work is

The resource model tells you that something is a diagnosis or a lab result. It doesn't tell you which diagnosis or which lab result — that's the job of external terminology systems, and FHIR just gives you a slot to put a code in:

  • SNOMED CT for clinical findings, procedures, and diagnoses — the biggest, most granular, and most expensive to license properly.
  • LOINC for anything you'd call a lab result or a measurement — heart rate, glucose, a pathology panel.
  • RxNorm for medications, when you're in the US; most other regions have their own equivalent and you end up maintaining a mapping table whether you asked for one or not.
  • ICD-10 for diagnosis codes, mostly because billing and insurance systems still speak ICD, not SNOMED.

The catch is that these systems don't map cleanly onto each other. A SNOMED finding and its "equivalent" ICD-10 code frequently encode different clinical granularity — SNOMED might distinguish three subtypes of a condition that ICD-10 collapses into one code, or vice versa. If your product needs both a clinician-facing view (SNOMED) and a billing export (ICD-10), you cannot get there with a lookup table you write once. We built a dedicated terminology service specifically so that this mapping logic — and the periodic code-system updates, because these systems version and deprecate codes on their own schedules — lived in exactly one place instead of being reinvented in every service that touched a diagnosis field.

The other terminology lesson: bind your CodeableConcept fields to an actual value set with a required or extensible binding strength as early as possible. FHIR's default posture is "anything goes" — a CodeableConcept will happily accept free text with no coding at all — and that flexibility is exactly what makes downstream search, analytics, and clinical decision support fall apart six months in, once half your data has a proper LOINC code and the other half has whatever string a form free-typed into existence.

Extensions are a feature, but treat them like a liability

The base FHIR resources cover the common case deliberately narrowly, and anything specific to your workflow goes into extension. This is the right design — it keeps the core spec small — but extensions have no compile-time safety and no discoverability beyond documentation you have to maintain yourself. We keep a single internal registry mapping every extension URL we've defined to the resource types it's valid on and the code that reads it, and we review it in the same PR that adds a new one. Skip that discipline and six months in you'll have three different extensions doing almost the same thing, defined by three different people who didn't know about each other.

Why a GraphQL layer sits in front of the FHIR API

This is the part that surprises people who've only read the FHIR spec: you basically never expose the raw FHIR REST API to a real frontend. FHIR's REST API is resource-shaped, not product-shaped — a patient summary screen needs the Patient, their three most recent Encounters, the Observations tied to each, and the Practitioner for each — and fetching that through raw FHIR search means either an N+1 chain of requests or a _include/_revinclude query gymnastics session that changes shape depending on what the screen needs. We put a GraphQL/BFF layer between the FHIR store and every client: it aggregates the resources a given view actually needs, applies business logic and validation FHIR doesn't and shouldn't enforce, caches aggressively where the data allows it, and gives the frontend a shape that matches the product instead of the spec. The FHIR server stays a clean, spec-compliant source of truth; the BFF is where "which fields does the appointment-booking screen actually need" lives, and that question changes far more often than the resource model does.

Auth is not a FHIR problem, but you'll solve it while solving FHIR

SMART on FHIR — the de facto standard for FHIR app authorization — is built on OAuth 2.1 with PKCE, and if you're building anything beyond a toy, you inherit its scoping model whether or not you use SMART's launch context flows. In a multi-tenant product this compounds fast: every request needs to resolve not just "which user" but "which organization, with what role, scoped to what resource types," and that check has to happen consistently across every service that touches patient data, not just at the API gateway. We built this on top of BetterAuth with JWT/JWKS for service-to-service calls and RBAC with organization-level scoping for everything else, and the thing I'd emphasize to anyone starting this today: get the tenant-scoping check into one shared piece of middleware immediately. The moment it's reimplemented per-service is the moment someone gets it subtly wrong in exactly one place, and in healthcare that's not a bug ticket, it's an incident.

What I'd tell someone starting today

Read the spec, but don't trust it to make your modeling decisions for you — it was written to accommodate every possible implementer, not to give you good defaults. Pick your terminology bindings and your extension conventions before you've written much code, because loosening a schema is free and tightening one after you have data in it is not. Put a real application layer between FHIR and your product; the spec was never trying to be your API design, and it will fight you if you treat it as one. And budget real time for terminology — not because it's exotic, but because it's the part of the system that has to be right for everything built on top of it to mean what it says.

FHIR earns its reputation as the right foundation for healthcare data. It just doesn't earn it for free.

Share: