Bắt đầu với FHIR cho lập trình viên — 30 phút từ zero
Trong 30 phút, bạn stand up FHIR server, tạo Patient Việt Nam đầu tiên, search được, validate theo Profile VN Core, và build Bundle transaction — không cần biết HL7 v2 trước.
Trang này là tutorial code FHIR tutorial việt nam dành cho DEV web/mobile đã quen REST + JSON. Mỗi bài 5–10 phút, có code mẫu cho cả bốn ngôn ngữ thường dùng (cURL, JavaScript, Python, Java HAPI). Toàn bộ payload đều copy-paste-runnable trên HAPI public sandbox hoặc Docker self-host.
Tóm tắt nhanh
- 30 phút × 4 bài: Hello World → Search → Profile VN Core validate → Bundle transaction.
- Stack: HAPI FHIR public sandbox
http://hapi.fhir.org/baseR4(free) hoặc Dockerhapiproject/hapi:latestself-host. - Sample đủ 4 ngôn ngữ: cURL, JavaScript (fetch), Python (fhirpy), Java (HAPI client).
- Identifier URI VN Core:
http://fhir.hl7.org.vn/core/sid/cccd— dùng nhất quán mọi nơi. - Cài VN Core IG vào HAPI bằng
application.yamlauto-load (KHÔNG có CLIupload-definitions).
1. Setup môi trường (5 phút)
FHIR R4 (4.0.1, 146 resources, Mixed Normative + STU) là REST API trả JSON theo schema chuẩn. Để học nhanh, không cần dựng cluster — chọn một trong hai option dưới đây và có endpoint sống trong vòng 5 phút.
Option A — HAPI FHIR public sandbox (1 phút)
HAPI FHIR là server FHIR open-source phổ biến nhất, viết bằng Java. Team HAPI host sẵn một sandbox công cộng tại http://hapi.fhir.org/baseR4: không cần đăng ký, không cần auth. Phù hợp để chạy thử các curl đầu tiên, nhưng dữ liệu shared toàn cầu, có thể bị reset bất kỳ lúc nào — KHÔNG dùng cho production hoặc dữ liệu thật của bệnh nhân.
curl http://hapi.fhir.org/baseR4/metadata | head -50
Lệnh trên trả về CapabilityStatement — bản tự khai báo của server liệt kê resource nào được hỗ trợ và search parameter nào hợp lệ.
Option B — Docker self-host (5 phút)
Để có endpoint riêng (data không bị share, có thể nạp Implementation Guide), chạy image chính thức hapiproject/hapi:latest:
docker run -d -p 8080:8080 \
--name hapi-fhir \
hapiproject/hapi:latest
# Đợi ~30 giây để HAPI khởi động xong
curl http://localhost:8080/fhir/metadata | head -20
Endpoint base: http://localhost:8080/fhir. Mọi ví dụ dưới đây sẽ dùng endpoint này; nếu bạn dùng public sandbox, đổi sang http://hapi.fhir.org/baseR4.
2. Bài 1 — Hello World: tạo Patient Việt Nam (5 phút)
Resource Patient là điểm khởi đầu kinh điển của mọi FHIR tutorial. Bài này tạo một Patient nữ, 40 tuổi, ở Hà Nội, với số CCCD 12 chữ số đúng quy ước Việt Nam. Lưu ý identifier.system phải là URI VN Core http://fhir.hl7.org.vn/core/sid/cccd — đây là canonical chính thức của VN Core IG, dùng nhất quán cho mọi POST và mọi query search về sau.
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"]
}]
}'
Server trả HTTP 201 Created; header Location chứa URL kèm id server vừa cấp, ví dụ http://localhost:8080/fhir/Patient/12345/_history/1. Body response là full Patient có thêm meta.versionId và meta.lastUpdated. Đây là toàn bộ Hello World — bạn vừa tạo xong một resource FHIR R4 hợp lệ.
Mẹo: nếu thấy lỗi 415 Unsupported Media Type, kiểm tra header Content-Type: application/fhir+json — đây là MIME type bắt buộc, không phải application/json thông thường.
3. Bài 2 — Search Patient (5 phút)
FHIR có cơ chế search rất giàu, hỗ trợ filter theo identifier, tên, ngày sinh, giới tính, và combinable bằng query string thông thường. Với CCCD ta đã tạo, query search phải dùng đúng identifier system mà ta đã set lúc POST — sai system một ký tự là search trả rỗng.
# Search theo CCCD (system|value — phải khớp tuyệt đối với system khi POST)
curl "http://localhost:8080/fhir/Patient?identifier=http://fhir.hl7.org.vn/core/sid/cccd|001234567890"
# Search theo họ
curl "http://localhost:8080/fhir/Patient?family=Nguyễn"
# Search nữ sinh trong khoảng 1980-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"
Response là một Bundle với type="searchset". Mỗi entry chứa một Patient match. Để lấy trang tiếp theo, đọc Bundle.link với relation="next" — KHÔNG tự sinh URL pagination, vì server có thể dùng cursor hoặc offset tuỳ implementation.
Search modifier hữu ích: :exact (so chính xác, phân biệt hoa thường), :contains (substring), :missing=true (lọc resource thiếu field). Prefix cho ngày/số: eq (mặc định), ne, gt, ge, lt, le.
4. Bài 3 — Profile VN Core validate (5 phút)
Patient ở Bài 1 là FHIR base — server chấp nhận miễn JSON đúng schema R4. Nhưng để đảm bảo dữ liệu đáp ứng quy ước Việt Nam (bắt buộc CCCD, đúng dân tộc, địa chỉ có cấp xã/phường), cần validate theo Profile VN Core. Profile chính thức tại canonical http://fhir.hl7.org.vn/core/StructureDefinition/vn-core-patient.
Cài VN Core IG vào HAPI
Lưu ý quan trọng: HAPI FHIR CLI không có subcommand upload-definitions (đây là lỗi phổ biến trong nhiều tutorial cũ). Cách đúng là cấu hình HAPI JPA Starter để auto-load Implementation Guide qua application.yaml, hoặc download package .tgz rồi PUT từng StructureDefinition qua REST.
Cách 1 — Auto-load qua application.yaml (khuyến nghị):
# application.yaml (mount vào /app/config/application.yaml trong 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 container — HAPI tự download package, parse, và nạp toàn bộ StructureDefinition + ValueSet + CodeSystem vào registry validation.
Cách 2 — PUT thủ công từng StructureDefinition:
# Download 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 từng StructureDefinition (id từ tên file)
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 Patient theo Profile
FHIR có operation chuẩn $validate để kiểm tra một resource có hợp lệ với profile cho trước hay không:
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
Server trả OperationOutcome với array issue:
severity="error"→ Patient vi phạm constraint (ví dụ: thiếu CCCD, giới tính không thuộc value set, dân tộc sai code).severity="warning"→ còn thiếu field Must Support nhưng không bắt buộc.severity="information"hoặc OperationOutcome rỗng issue → pass.
5. Bài 4 — Bundle transaction (10 phút)
Trong thực tế, hiếm khi tạo từng resource lẻ. Use case điển hình: tiếp nhận bệnh nhân ngoại trú → tạo Patient + Encounter + Observation đo nhịp tim, atomic — nếu một entry fail, rollback tất cả. FHIR cung cấp Bundle với type="transaction" đúng cho mục đích này.
Mẹo quan trọng: dùng fullUrl dạng urn:uuid: để các entry tham chiếu nội bộ với nhau trước khi server cấp id thật. Server sẽ tự rewrite reference urn:uuid:patient-1 thành Patient/<id-thật> sau khi 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"}
}
]
} Gửi Bundle bằng POST vào root endpoint:
curl -X POST http://localhost:8080/fhir \
-H "Content-Type: application/fhir+json" \
-d @bundle-transaction.json
Response là một Bundle khác với type="transaction-response", mỗi entry có response.status (201 Created) và response.location (URL resource đã tạo). Nếu một entry fail, toàn bộ bundle rollback và server trả OperationOutcome với severity error.
Encounter dùng class.system = "http://terminology.hl7.org/CodeSystem/v3-ActCode" với code AMB (ambulatory) — đây là CodeSystem v3 chuẩn HL7, KHÔNG phải URL placeholder. Observation dùng LOINC 8867-4 cho heart rate và UCUM /min cho đơn vị.
6. Sample 4 ngôn ngữ — tạo Patient VN
Cùng nghiệp vụ Bài 1 (tạo Patient nữ, CCCD 001234567890), bốn implementation song song để bạn chọn ngôn ngữ phù hợp. cURL đã có ở Bài 1. Ba sample còn lại dùng client mainstream nhất hiện nay.
JavaScript / TypeScript (fetch thuần)
Thư viện fhir.js bị archive từ 2023 và API không còn được maintain — khuyến nghị dùng fetch thuần (Node 18+, browser modern). Nếu cần object model FHIR, kết hợp với package fhir hoặc @types/fhir chỉ cho TypeScript types.
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 (beda-software) là client async/sync phổ biến nhất cho Python, hỗ trợ CRUD, search, và validate. Cài đặt: 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 cung cấp luôn cả server và client trong cùng SDK. Maven dependency: 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. Tooling khuyến nghị
Hệ sinh thái FHIR R4 đã trưởng thành, phần lớn tool đều free/open-source. Dưới đây là các tool DEV nên bookmark cho từng giai đoạn — server, client, validation, profile authoring.
| Tool | Mục đích | License |
|---|---|---|
| HAPI FHIR | Server + client (Java) — chuẩn de-facto | Apache 2.0 |
| Microsoft FHIR Server | Server cho Azure / .NET stack | MIT |
| Firely .NET SDK | Client + parser cho .NET | BSD-3 |
| fhirpy | Client Python (async + sync) | MIT |
| fhir.resources | Pydantic models cho FHIR | BSD-3 |
| FHIR Validator (CLI) | Validate offline với IG | Apache 2.0 |
| SUSHI | Compiler FSH → FHIR JSON | Apache 2.0 |
| Postman + FHIR collection | Manual testing REST | Free tier |
8. 10 lỗi DEV mới hay gặp
Đây là các lỗi xuất hiện trong tuần đầu tiên của hầu hết DEV mới làm FHIR. Mỗi lỗi đi kèm cách phát hiện và fix nhanh.
- Quên header
Content-Type: application/fhir+json→ server trả 415 Unsupported Media Type.application/jsonKHÔNG được chấp nhận theo spec. - Identifier
systemkhông match giữa POST và search → search trả Bundle rỗng dù resource đã tồn tại. Luôn dùng URI canonicalhttp://fhir.hl7.org.vn/core/sid/cccd. birthDatesai format (gửi15/03/1985thay vì1985-03-15) → 422 Unprocessable Entity. FHIR luôn dùng ISO 8601.- Reference relative vs absolute lẫn lộn: trong Bundle transaction phải dùng
urn:uuid:..., ngoài Bundle dùngPatient/123. Trộn lẫn là server không resolve được. - Bundle entry quên
request→ HAPI báo "Missing required element 'request'". Mỗi entry transaction PHẢI córequest.methodvàrequest.url. - Validate
$validatemà chưa load IG vào server → OperationOutcome báo "Profile reference cannot be resolved". Cấu hìnhapplication.yamlhoặc PUT StructureDefinition trước. - Search modifier dùng sai cú pháp: phải là
name:exact=Lan, KHÔNG phảiname=:exact:Lan. Modifier nằm sau parameter name, ngăn cách bằng dấu hai chấm. - Pagination tự sinh URL bằng
_count+_offset: nhiều server không hỗ trợ_offset. Phải đọcBundle.link[relation="next"]server trả về. - Nhầm
meta.securityvớimeta.tag:securitylà access control label,taglà workflow flag tự do. Search behavior khác nhau. - Không set
Accept-Charset: utf-8hoặc gửi body không UTF-8: tiếng Việt có dấu lưu sai (Nguy?n thay vì Nguyễn). FHIR JSON luôn UTF-8 — đảm bảo client encode đúng vàContent-Typekhông khai charset khác.
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/
Trang này được biên soạn bởi nhóm OmiGroup (Omi HealthTech), reviewer kỹ thuật HungPM (Phan Mạnh Hùng), với fix factual từ Codex review (HAPI CLI semantics, identifier URI, Encounter.class CodeSystem) và Gemini review (language polish).