HTTP & JSON for health APIs
The two technologies underneath every modern health API. What the status codes actually mean when the payload is a patient, and why the boring details decide whether your integration is safe.
In one line
Every modern health API is HTTP moving JSON. Both are simple enough to learn in an afternoon and subtle enough that the details decide whether your integration loses a patient's allergy.
The verbs, and what they promise
- GET — read. Safe: it changes nothing, so it can be retried, cached, logged freely.
- POST — create. Not idempotent — call it twice, create two things.
- PUT — replace at a known id. Idempotent: call it ten times, same result.
- PATCH — partial update.
- DELETE — remove. Idempotent in principle.
The word that matters clinically is idempotent: does doing it twice equal doing it once?
Consider a flaky network. Your app POSTs an Observation. The response times out. Did it land? You don't know. Retry, and you may have just created a duplicate lab result on a patient's chart — which a clinician will read as two tests. Retry a PUT and nothing bad happens.
That is not academic. Duplicate clinical records created by naive retries are a real and common
failure mode, and the fix is architectural: use conditional creates (If-None-Exist in FHIR),
or client-assigned ids, so the second attempt is recognisably the same attempt.
Status codes, honestly
The ones that carry weight in health integrations:
| Code | Means | The trap |
|---|---|---|
| 200 | OK | An empty FHIR Bundle is a 200. "No allergies found" and "the query was wrong" look identical. |
| 201 | Created | Read the Location header — that's your id. |
| 400 | Your request is malformed | You broke it. Read OperationOutcome. |
| 401 | Not authenticated | Your token is missing/expired. |
| 403 | Authenticated, not allowed | You are who you say and still may not. Different fix entirely. |
| 404 | Not found | Or: exists and you may not know it exists. Deliberate ambiguity. |
| 409 | Conflict | Someone changed it since you read it. |
| 412 | Precondition failed | Your If-Match version was stale. |
| 422 | Well-formed, semantically wrong | Valid JSON, invalid FHIR. |
| 429 | Rate limited | Back off. Respect Retry-After. |
| 5xx | Their problem | Retry with backoff — but only if idempotent. |
The 200-with-empty-result case is the one that hurts in health. Your allergy check queries the API, gets a 200 and an empty bundle, and displays "No known allergies." Was that true, or did the search parameter not match? A false "no allergies" is a clinical statement your code made up. Distinguish no data from no answer — and if you can't, say "unknown", not "none".
Concurrency: the lost update
Two clinicians open the same medication list. Both edit. Both save. The second silently overwrites the first, and the first clinician's change is gone with no error anywhere.
HTTP already solved this: ETags and If-Match. Read the resource, note its version, and
send it back with your write. If it changed underneath you, the server returns 412 and you
handle it — rather than destroying someone's work.
FHIR builds this in via versionId and supports version-aware updates. Use it. Optimistic
locking is one line of code and the difference between a data-loss bug and a conflict dialog.
JSON's health-specific gotchas
- Numbers. JSON numbers are IEEE 754 doubles. A long identifier — an ABHA number, an accession — will lose precision. Identifiers are strings. Always. This is the same disease as Excel mangling long numbers, from a different direction.
nullvs absent vs empty. Three different things people treat as one. In clinical data "not recorded", "recorded as none", and "not applicable" are genuinely different, and collapsing them loses meaning.- Key order isn't guaranteed — don't sign or hash raw JSON without canonicalising.
- No date type. Dates are strings, and a timezone-less timestamp on a medication administration is an ambiguity you'll pay for.
The unglamorous rule
Read the OperationOutcome. FHIR servers
explain their errors in it, and the number of hours lost to teams logging "400 Bad Request" while
the body contained the exact reason is genuinely tragic.