FHIR for developers — get started in 30 minutes from zero
In 30 minutes, you will stand up a FHIR server, create your first Vietnamese Patient, run searches, validate against the VN Core profile, and build a Bundle transaction — no prior HL7 v2 knowledge required.
This page is a hands-on FHIR tutorial for web and mobile developers already comfortable with REST and JSON. Each lesson takes 5–10 minutes and ships code samples for the four most common languages (cURL, JavaScript, Python, Java HAPI). Every payload is copy-paste-runnable on the HAPI public sandbox or a Docker self-host.
TL;DR
- 30 minutes across 4 lessons: Hello World → Search → VN Core profile validation → Bundle transaction.
- Stack: HAPI FHIR public sandbox
http://hapi.fhir.org/baseR4(free) or Dockerhapiproject/hapi:latestfor self-hosting. - Samples in all four languages: cURL, JavaScript (fetch), Python (fhirpy), Java (HAPI client).
- VN Core identifier URI:
http://fhir.hl7.org.vn/core/sid/cccd— used consistently everywhere. - Install the VN Core IG into HAPI via
application.yamlauto-load (there is NOupload-definitionsCLI subcommand).
On this page
- Set up your environment (5 minutes)
- Lesson 1 — Hello World: create a Vietnamese Patient
- Lesson 2 — Search Patient
- Lesson 3 — Validate against the VN Core profile
- Lesson 4 — Bundle transaction
- Samples in 4 languages
- Recommended tooling
- 10 common mistakes new developers make
- References
- Further reading
1. Set up your environment (5 minutes)
FHIR R4 (4.0.1, 146 resources, mixed Normative + STU) is a REST API that returns JSON conforming to a standard schema. To learn quickly you do not need to spin up a cluster — pick one of the two options below and you will have a live endpoint in under 5 minutes.
Option A — HAPI FHIR public sandbox (1 minute)
HAPI FHIR is the most popular open-source FHIR server, written in Java. The HAPI team hosts a public sandbox at http://hapi.fhir.org/baseR4: no signup, no auth required. It is great for running your first curl commands, but the data is shared globally and may be reset at any time — DO NOT use it for production or real patient data.
curl http://hapi.fhir.org/baseR4/metadata | head -50
That command returns a CapabilityStatement — the server's self-declaration listing which resources it supports and which search parameters are valid.
Option B — Docker self-host (5 minutes)
For your own endpoint (data is not shared and you can load Implementation Guides), run the official hapiproject/hapi:latest image:
docker run -d -p 8080:8080 \
--name hapi-fhir \
hapiproject/hapi:latest
# Wait ~30 seconds for HAPI to finish booting
curl http://localhost:8080/fhir/metadata | head -20
The base endpoint is http://localhost:8080/fhir. Every example below uses this endpoint; if you are on the public sandbox, swap to http://hapi.fhir.org/baseR4.
2. Lesson 1 — Hello World: create a Vietnamese Patient (5 minutes)
The Patient resource is the canonical starting point of every FHIR tutorial. This lesson creates a 40-year-old female Patient in Hanoi with a 12-digit national ID card (CCCD) following Vietnamese conventions. Note that identifier.system must be the VN Core URI http://fhir.hl7.org.vn/core/sid/cccd — this is the official canonical of the VN Core IG and must be used consistently for every POST and every search query later.
curl -X POST http://localhost:8080/fhir/Patient \
-H "Content-Type: application/fhir+json" \
-d '{
"resourceType": "Patient",
"identifier": [{
"system": "http://fhir.hl7.org.vn/core/sid/cccd",
"value": "001234567890"
}],
"name": [{"family": "Nguyễn", "given": ["Thị", "Lan"]}],
"gender": "female",
"birthDate": "1985-03-15",
"address": [{
"country": "VN",
"city": "Hà Nội",
"line": ["123 Lê Lợi, P. Bến Thành"]
}]
}'
The server returns HTTP 201 Created; the Location header contains the URL with the freshly assigned id, for example http://localhost:8080/fhir/Patient/12345/_history/1. The response body is the full Patient resource with added meta.versionId and meta.lastUpdated. That is the entire Hello World — you have just created a valid FHIR R4 resource.
Tip: if you see HTTP 415 Unsupported Media Type, check the header Content-Type: application/fhir+json — that is the required MIME type, not the plain application/json.
3. Lesson 2 — Search Patient (5 minutes)
FHIR ships a very rich search mechanism with filters by identifier, name, birth date, gender, and any combination via standard query strings. With the CCCD we just created, the search query must use the exact identifier system we set on POST — getting one character of the system wrong yields an empty result.
# Search by CCCD (system|value — must match the system used on POST exactly)
curl "http://localhost:8080/fhir/Patient?identifier=http://fhir.hl7.org.vn/core/sid/cccd|001234567890"
# Search by family name
curl "http://localhost:8080/fhir/Patient?family=Nguyễn"
# Female patients born between 1980 and 1989
curl "http://localhost:8080/fhir/Patient?gender=female&birthdate=ge1980&birthdate=lt1990"
# Pagination + sort
curl "http://localhost:8080/fhir/Patient?family=Nguyễn&_count=10&_sort=birthdate"
The response is a Bundle with type="searchset". Each entry contains one matching Patient. To get the next page, follow Bundle.link with relation="next" — DO NOT hand-craft the pagination URL yourself, because servers may use cursors or offsets depending on implementation.
Useful search modifiers: :exact (exact match, case sensitive), :contains (substring), :missing=true (resources missing the field). Prefixes for date and number values: eq (default), ne, gt, ge, lt, le.
4. Lesson 3 — Validate against the VN Core profile (5 minutes)
The Patient from Lesson 1 is base FHIR — the server accepts it as long as the JSON conforms to the R4 schema. To make sure the data complies with Vietnamese conventions (mandatory CCCD, valid ethnicity, address with ward or commune level), you need to validate against the VN Core profile. The official profile lives at canonical http://fhir.hl7.org.vn/core/StructureDefinition/vn-core-patient.
Install the VN Core IG into HAPI
Important note: the HAPI FHIR CLI does not have an upload-definitions subcommand (this is a common error in older tutorials). The correct approach is to configure the HAPI JPA Starter to auto-load the Implementation Guide via application.yaml, or to download the .tgz package and PUT each StructureDefinition over REST.
Method 1 — Auto-load via application.yaml (recommended):
# application.yaml (mount into /app/config/application.yaml in the container)
hapi:
fhir:
implementationguides:
vn-core:
name: hl7.fhir.vn.core
version: 0.5.0
url: https://downloads.fhir.hl7.org.vn/core/0.5.0/hl7.fhir.vn.core-0.5.0.tgz Restart the container — HAPI downloads the package, parses it, and loads every StructureDefinition, ValueSet, and CodeSystem into its validation registry.
Method 2 — PUT each StructureDefinition manually:
# Download the IG package
curl -L -o vn-core.tgz \
https://downloads.fhir.hl7.org.vn/core/0.5.0/hl7.fhir.vn.core-0.5.0.tgz
tar xzf vn-core.tgz
# PUT each StructureDefinition (id derived from filename)
for sd in package/StructureDefinition-*.json; do
id=$(basename "$sd" .json | sed 's/StructureDefinition-//')
curl -X PUT -H "Content-Type: application/fhir+json" \
--data-binary @"$sd" \
"http://localhost:8080/fhir/StructureDefinition/$id"
done Validate a Patient against the profile
FHIR ships a standard $validate operation to check whether a resource is valid against a given profile:
curl -X POST \
"http://localhost:8080/fhir/Patient/$validate?profile=http://fhir.hl7.org.vn/core/StructureDefinition/vn-core-patient" \
-H "Content-Type: application/fhir+json" \
-d @patient-vn.json
The server returns an OperationOutcome with an issue array:
severity="error"→ the Patient violates a constraint (e.g., missing CCCD, gender outside the value set, invalid ethnicity code).severity="warning"→ a Must Support field is missing but not strictly required.severity="information"or an OperationOutcome with no issues → pass.
5. Lesson 4 — Bundle transaction (10 minutes)
In the real world, you rarely create resources one at a time. A typical use case: an outpatient visit → create Patient + Encounter + Observation (heart rate), atomically — if any entry fails, roll back everything. FHIR provides Bundle with type="transaction" for exactly this.
Key tip: use a fullUrl in the urn:uuid: form so entries can reference each other before the server assigns real ids. The server rewrites references like urn:uuid:patient-1 to Patient/<real-id> after the commit.
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:patient-1",
"resource": {
"resourceType": "Patient",
"identifier": [{
"system": "http://fhir.hl7.org.vn/core/sid/cccd",
"value": "001234567890"
}],
"name": [{"family": "Nguyễn", "given": ["Thị", "Lan"]}],
"gender": "female",
"birthDate": "1985-03-15"
},
"request": {"method": "POST", "url": "Patient"}
},
{
"fullUrl": "urn:uuid:encounter-1",
"resource": {
"resourceType": "Encounter",
"status": "in-progress",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "AMB",
"display": "ambulatory"
},
"subject": {"reference": "urn:uuid:patient-1"},
"period": {"start": "2026-04-30T08:00:00+07:00"}
},
"request": {"method": "POST", "url": "Encounter"}
},
{
"fullUrl": "urn:uuid:obs-1",
"resource": {
"resourceType": "Observation",
"status": "final",
"code": {
"coding": [{
"system": "http://loinc.org",
"code": "8867-4",
"display": "Heart rate"
}]
},
"subject": {"reference": "urn:uuid:patient-1"},
"encounter": {"reference": "urn:uuid:encounter-1"},
"valueQuantity": {
"value": 78,
"unit": "/min",
"system": "http://unitsofmeasure.org",
"code": "/min"
}
},
"request": {"method": "POST", "url": "Observation"}
}
]
} Submit the Bundle with a POST to the root endpoint:
curl -X POST http://localhost:8080/fhir \
-H "Content-Type: application/fhir+json" \
-d @bundle-transaction.json
The response is another Bundle with type="transaction-response"; each entry has a response.status (201 Created) and a response.location (URL of the created resource). If any entry fails, the entire bundle rolls back and the server returns an OperationOutcome with severity error.
Encounter uses class.system = "http://terminology.hl7.org/CodeSystem/v3-ActCode" with code AMB (ambulatory) — that is the standard HL7 v3 CodeSystem, NOT a placeholder URL. Observation uses LOINC code 8867-4 for heart rate and the UCUM unit /min.
6. Samples in 4 languages — create a Vietnamese Patient
Same business case as Lesson 1 (create a female Patient with CCCD 001234567890), four parallel implementations so you can pick the language that fits your stack. cURL is already covered in Lesson 1. The remaining three samples use today's most mainstream clients.
JavaScript / TypeScript (plain fetch)
The fhir.js library was archived back in 2023 and its API is no longer maintained — we recommend plain fetch (Node 18+ and modern browsers). If you need a FHIR object model, pair it with the fhir package or with @types/fhir for TypeScript types only.
const baseUrl = 'http://localhost:8080/fhir';
const patient = {
resourceType: 'Patient',
identifier: [{
system: 'http://fhir.hl7.org.vn/core/sid/cccd',
value: '001234567890',
}],
name: [{ family: 'Nguyễn', given: ['Thị', 'Lan'] }],
gender: 'female',
birthDate: '1985-03-15',
};
const res = await fetch(`${baseUrl}/Patient`, {
method: 'POST',
headers: { 'Content-Type': 'application/fhir+json' },
body: JSON.stringify(patient),
});
if (!res.ok) {
throw new Error(`FHIR error ${res.status}: ${await res.text()}`);
}
const created = await res.json();
console.log('Created Patient id:', created.id); Python (fhirpy)
fhirpy (by beda-software) is the most popular async/sync Python client, with support for CRUD, search, and validation. Install with pip install fhirpy.
from fhirpy import SyncFHIRClient
client = SyncFHIRClient('http://localhost:8080/fhir')
patient = client.resource(
'Patient',
identifier=[{
'system': 'http://fhir.hl7.org.vn/core/sid/cccd',
'value': '001234567890',
}],
name=[{'family': 'Nguyễn', 'given': ['Thị', 'Lan']}],
gender='female',
birthDate='1985-03-15',
)
patient.save()
print('Created Patient id:', patient.id) Java (HAPI FHIR client)
HAPI FHIR ships both server and client in the same SDK. Maven dependencies: ca.uhn.hapi.fhir:hapi-fhir-structures-r4 + ca.uhn.hapi.fhir:hapi-fhir-client.
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.hl7.fhir.r4.model.*;
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");
Patient patient = new Patient();
patient.addIdentifier()
.setSystem("http://fhir.hl7.org.vn/core/sid/cccd")
.setValue("001234567890");
patient.addName()
.setFamily("Nguyễn")
.addGiven("Thị")
.addGiven("Lan");
patient.setGender(Enumerations.AdministrativeGender.FEMALE);
patient.setBirthDateElement(new DateType("1985-03-15"));
MethodOutcome outcome = client.create().resource(patient).execute();
System.out.println("Created Patient id: " + outcome.getId().getIdPart()); 7. Recommended tooling
The FHIR R4 ecosystem is mature and most tools are free or open-source. Below are the tools developers should bookmark for each phase — server, client, validation, and profile authoring.
| Tool | Purpose | License |
|---|---|---|
| HAPI FHIR | Server + client (Java) — the de facto standard | Apache 2.0 |
| Microsoft FHIR Server | Server for the Azure / .NET stack | MIT |
| Firely .NET SDK | Client and parser for .NET | BSD-3 |
| fhirpy | Python client (async + sync) | MIT |
| fhir.resources | Pydantic models for FHIR | BSD-3 |
| FHIR Validator (CLI) | Offline validation against IGs | Apache 2.0 |
| SUSHI | FSH → FHIR JSON compiler | Apache 2.0 |
| Postman + FHIR collection | Manual REST testing | Free tier |
8. 10 common mistakes new developers make
These are the errors most developers hit during their first week with FHIR. Each one comes with how to spot it and a quick fix.
- Forgetting the
Content-Type: application/fhir+jsonheader → the server returns 415 Unsupported Media Type. Plainapplication/jsonis NOT accepted by the spec. - Identifier
systemmismatch between POST and search → search returns an empty Bundle even though the resource exists. Always use the canonical URIhttp://fhir.hl7.org.vn/core/sid/cccd. - Wrong
birthDateformat (sending15/03/1985instead of1985-03-15) → 422 Unprocessable Entity. FHIR always uses ISO 8601. - Mixing relative and absolute references: inside a transaction Bundle you must use
urn:uuid:...; outside a Bundle usePatient/123. Mixing them prevents the server from resolving references. - Bundle entry missing
request→ HAPI reports "Missing required element 'request'". Every transaction entry MUST haverequest.methodandrequest.url. - Calling
$validatebefore loading the IG into the server → OperationOutcome reports "Profile reference cannot be resolved". Configureapplication.yamlor PUT the StructureDefinitions first. - Wrong search modifier syntax: it is
name:exact=Lan, NOTname=:exact:Lan. The modifier sits after the parameter name, separated by a colon. - Hand-rolling pagination URLs with
_count+_offset: many servers do not support_offset. Always followBundle.link[relation="next"]from the server. - Confusing
meta.securitywithmeta.tag:securityis an access-control label, whiletagis a free-form workflow flag. Their search behaviors differ. - Skipping
Accept-Charset: utf-8or sending a non-UTF-8 body: Vietnamese diacritics get garbled (Nguy?n instead of Nguyễn). FHIR JSON is always UTF-8 — make sure the client encodes correctly and thatContent-Typedoes not declare a different charset.
9. References
- HL7 FHIR R4 spec — https://hl7.org/fhir/R4/
- FHIR RESTful API — https://hl7.org/fhir/R4/http.html
- Bundle transaction semantics — https://hl7.org/fhir/R4/http.html#transaction
- Encounter.class CodeSystem v3 ActCode — https://terminology.hl7.org/CodeSystem-v3-ActCode.html
- HAPI FHIR docs — https://hapifhir.io/hapi-fhir/docs/
- HAPI FHIR public sandbox — http://hapi.fhir.org/
- HAPI JPA Starter (Docker, application.yaml) — github.com/hapifhir/hapi-fhir-jpaserver-starter
- fhirpy (Python client) — github.com/beda-software/fhir-py
- fhir.resources (Pydantic) — github.com/nazrulworld/fhir.resources
- LOINC — https://loinc.org/ · UCUM — https://ucum.org/
- VN Core IG (canonical) — http://fhir.hl7.org.vn/core/
This page was authored by the OmiGroup team (Omi HealthTech), with technical review by HungPM (Phan Mạnh Hùng), factual fixes from a Codex review (HAPI CLI semantics, identifier URI, Encounter.class CodeSystem), and language polish from a Gemini review.