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 Docker hapiproject/hapi:latest self-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.yaml auto-load (KHÔNG có CLI upload-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.versionIdmeta.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 FHIRServer + client (Java) — chuẩn de-factoApache 2.0
Microsoft FHIR ServerServer cho Azure / .NET stackMIT
Firely .NET SDKClient + parser cho .NETBSD-3
fhirpyClient Python (async + sync)MIT
fhir.resourcesPydantic models cho FHIRBSD-3
FHIR Validator (CLI)Validate offline với IGApache 2.0
SUSHICompiler FSH → FHIR JSONApache 2.0
Postman + FHIR collectionManual testing RESTFree 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.

  1. Quên header Content-Type: application/fhir+json → server trả 415 Unsupported Media Type. application/json KHÔNG được chấp nhận theo spec.
  2. Identifier system không match giữa POST và search → search trả Bundle rỗng dù resource đã tồn tại. Luôn dùng URI canonical http://fhir.hl7.org.vn/core/sid/cccd.
  3. birthDate sai format (gửi 15/03/1985 thay vì 1985-03-15) → 422 Unprocessable Entity. FHIR luôn dùng ISO 8601.
  4. Reference relative vs absolute lẫn lộn: trong Bundle transaction phải dùng urn:uuid:..., ngoài Bundle dùng Patient/123. Trộn lẫn là server không resolve được.
  5. Bundle entry quên request → HAPI báo "Missing required element 'request'". Mỗi entry transaction PHẢI có request.methodrequest.url.
  6. Validate $validate mà chưa load IG vào server → OperationOutcome báo "Profile reference cannot be resolved". Cấu hình application.yaml hoặc PUT StructureDefinition trước.
  7. Search modifier dùng sai cú pháp: phải là name:exact=Lan, KHÔNG phải name=:exact:Lan. Modifier nằm sau parameter name, ngăn cách bằng dấu hai chấm.
  8. Pagination tự sinh URL bằng _count + _offset: nhiều server không hỗ trợ _offset. Phải đọc Bundle.link[relation="next"] server trả về.
  9. Nhầm meta.security với meta.tag: security là access control label, tag là workflow flag tự do. Search behavior khác nhau.
  10. Không set Accept-Charset: utf-8 hoặ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-Type không khai charset khác.

9. References

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).