FHIR cho lập trình viên triển khai — REST API, VN Core validation và Bundle
Bài này đưa lập trình viên từ REST API, payload Patient Việt Nam, search, Profile VN Core validation đến Bundle transaction, với trọng tâm là dữ liệu có thể kiểm chứng theo VN Core.
Trang này dành cho DEV web/mobile đã quen REST và JSON. Các ví dụ dùng endpoint cục bộ để người đọc kiểm soát cấu hình; public sandbox chỉ phù hợp với những interaction mà CapabilityStatement hiện thời của sandbox công bố và không được mặc định là đã cài VN Core.
Tóm tắt nhanh
- Lộ trình 4 phần: Hello World → Search → Profile VN Core validation → Bundle transaction.
- Môi trường: HAPI FHIR public test server hoặc HAPI JPA Starter cục bộ; luôn đọc
[base]/metadatatrước khi gọi interaction. - Sample đủ 4 ngôn ngữ: cURL, JavaScript (fetch), Python (fhirpy), Java (HAPI client).
- URI định danh CCCD của bản VN Core trial-use này là
http://fhir.hl7.org.vn/core/sid/cccd; client phải dùng đúng URI mà profile quy định. - Cách nạp package IG phụ thuộc phiên bản và cấu hình HAPI; phải xác nhận package đã được cài và profile thực sự được dùng khi validate.
1. Thiết lập và kiểm tra năng lực server
FHIR R4 (4.0.1) định nghĩa resource và nhiều cơ chế trao đổi, trong đó có RESTful API. Bài này chỉ dùng REST với biểu diễn JSON. Trước khi chạy ví dụ, chọn một endpoint thử nghiệm và kiểm tra CapabilityStatement của đúng endpoint đó.
Option A — HAPI FHIR public test server
HAPI FHIR công bố một endpoint thử nghiệm tại https://hapi.fhir.org/baseR4. Tình trạng, dữ liệu, cơ chế xác thực và năng lực của dịch vụ có thể thay đổi; không đưa dữ liệu cá nhân hoặc dữ liệu lâm sàng thật lên endpoint công cộng.
curl -fsS -H "Accept: application/fhir+json" \
https://hapi.fhir.org/baseR4/metadata
Trong CapabilityStatement.rest, kiểm tra mode, resource, interaction, search parameter và operation cần dùng. Đây là khai báo của server; test tích hợp vẫn phải kiểm tra mã HTTP, header và body thực tế.
Option B — HAPI JPA Starter chạy cục bộ
Có thể dùng image HAPI JPA Starter để tạo endpoint cục bộ. Tag latest thuận tiện cho thử nghiệm nhưng không tái lập được; dự án thực tế nên ghim tag hoặc digest đã được kiểm thử. Cấu hình mặc định là môi trường khởi đầu, không phải cấu hình production và không nên nhận dữ liệu thật khi chưa bổ sung xác thực, phân quyền, audit và các kiểm soát vận hành.
docker run -d -p 8080:8080 \
--name hapi-fhir \
hapiproject/hapi:latest
# Chỉ tiếp tục khi endpoint trả CapabilityStatement
curl -f -H "Accept: application/fhir+json" \
http://localhost:8080/fhir/metadata
Các ví dụ sau dùng http://localhost:8080/fhir. Không thay máy móc bằng public endpoint: chỉ chạy create, search, transaction hoặc $validate khi CapabilityStatement và phép thử thực tế xác nhận server hỗ trợ.
2. Bài 1 — Tạo Patient Việt Nam
Ví dụ tạo một Patient với số CCCD giả lập 12 chữ số. Khi biểu diễn CCCD theo bản VN Core trial-use tại site này, identifier.system dùng URI http://fhir.hl7.org.vn/core/sid/cccd. URI này thuộc namespace định danh; canonical của profile Patient là một URL khác trong StructureDefinition.
curl -X POST http://localhost:8080/fhir/Patient \
-H "Content-Type: application/fhir+json" \
-H "Accept: application/fhir+json" \
-H "Prefer: return=representation" \
-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"]
}]
}'
Nếu server hỗ trợ interaction create và xử lý thành công, FHIR R4 yêu cầu phản hồi HTTP 201 Created và header Location. Location có thể là URL tương đối hoặc tuyệt đối và không nhất thiết có phần _history nếu server không hỗ trợ versioning. Header Prefer: return=representation yêu cầu server trả resource trong body; nếu bỏ header này, server có thể trả body hoặc không. Hãy kiểm tra cả header lẫn body thay vì mặc định luôn có meta.versionId.
Lưu ý: application/fhir+json là media type chính thức cho FHIR JSON. Một số server có thể chấp nhận application/json, và FHIR cho phép client dùng media type tổng quát này trong Accept; không được suy ra mọi server sẽ xử lý request giống nhau. Nếu nhận 415, đọc OperationOutcome và tài liệu của endpoint.
3. Bài 2 — Search Patient
FHIR định nghĩa search framework và các search parameter chuẩn, nhưng server R4 không bắt buộc hỗ trợ mọi parameter hay modifier. Trước khi chạy các query dưới đây, kiểm tra server khai báo interaction search-type cho Patient cùng các parameter tương ứng. Khi tìm theo token identifier, dùng đúng cặp system–value đã lưu.
# --data-urlencode tránh lỗi với dấu | và ký tự tiếng Việt
curl --get "http://localhost:8080/fhir/Patient" \
--data-urlencode "identifier=http://fhir.hl7.org.vn/core/sid/cccd|001234567890"
# Search theo họ
curl --get "http://localhost:8080/fhir/Patient" \
--data-urlencode "family=Nguyễn"
# Search nữ sinh trong khoảng 1980-1989
curl --get "http://localhost:8080/fhir/Patient" \
--data-urlencode "gender=female" \
--data-urlencode "birthdate=ge1980" \
--data-urlencode "birthdate=lt1990"
# Pagination + sort
curl --get "http://localhost:8080/fhir/Patient" \
--data-urlencode "family=Nguyễn" \
--data-urlencode "_count=10" \
--data-urlencode "_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.
Với string search, :exact yêu cầu khớp toàn bộ giá trị và phân biệt hoa thường. :contains, :missing, prefix ngày/số, _sort và cách xử lý chuỗi có thể phụ thuộc năng lực server; chỉ dùng sau khi đối chiếu CapabilityStatement, tài liệu endpoint và kết quả test.
4. Bài 3 — Validate theo Profile VN Core
Phản hồi create thành công chỉ cho biết server đã chấp nhận request theo cấu hình của nó; điều đó không chứng minh resource đạt VN Core. Muốn kiểm tra các ràng buộc của bản VN Core trial-use — gồm slice CCCD, định danh thay thế hoặc data-absent-reason trong trường hợp được phép, và các extension địa phương — validator phải nạp đúng package và version. Canonical profile là http://fhir.hl7.org.vn/core/StructureDefinition/vn-core-patient.
Cài VN Core IG vào HAPI
Lưu ý quan trọng: public HAPI test server không mặc định hỗ trợ VN Core. Cơ chế cài IG của HAPI thay đổi theo phiên bản và module được bật; ghim phiên bản HAPI, đối chiếu tài liệu của phiên bản đó và không coi việc lưu riêng vài StructureDefinition qua REST là tương đương với cài trọn package cùng dependency và terminology.
Ví dụ cấu hình cho HAPI JPA Starter có hỗ trợ cài package:
# application.yaml (mount vào /app/config/application.yaml trong container)
hapi:
fhir:
implementationguides:
vn-core:
name: hl7.fhir.vn.core
version: 0.8.0
packageUrl: https://downloads.fhir.hl7.org.vn/core/0.8.0/packages/hl7.fhir.vn.core-0.8.0.tgz
installMode: STORE_AND_INSTALL
Các key trên phải khớp với phiên bản HAPI đã ghim. Sau khi khởi động, đọc log lỗi, lấy trực tiếp StructureDefinition/vn-core-patient và chạy một ca validate âm đã biết trước. Chỉ kết luận package đã hoạt động khi các kiểm tra này thành công; việc container chạy không đủ chứng minh package đã được tải hoặc được validator sử dụng.
Kiểm tra artifact đã được cài:
# Endpoint này chỉ thành công khi artifact đã được lưu với logical id tương ứng
curl -fsS -H "Accept: application/fhir+json" \
http://localhost:8080/fhir/StructureDefinition/vn-core-patient Validate Patient theo Profile
FHIR định nghĩa operation $validate, nhưng server không bắt buộc triển khai operation này. Kiểm tra CapabilityStatement và gọi thử với đúng profile/version:
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" \
-H "Accept: application/fhir+json" \
-d @patient-vn.json
Đánh giá cả mã HTTP và OperationOutcome.issue; server có thể phân loại chi tiết khác nhau:
severity="fatal"hoặcerrorcho biết request/resource không đạt kiểm tra tương ứng; đọccode,details,diagnosticsvàexpression.warningvàinformationcần được xử lý theo chính sách của dự án; việc thiếu element Must Support không tự động tạo warning vì nghĩa Must Support do IG và use case quy định.- Không suy ra “pass” chỉ từ một issue
information. Xác nhận không cófatal/error, đúng canonical/version và validator đã resolve đầy đủ dependency.
5. Bài 4 — Bundle transaction
Ví dụ sau gửi Patient, Encounter và Observation trong một Bundle có type="transaction". Tính nguyên tử — tất cả thành công hoặc toàn bộ thay đổi bị hủy — chỉ áp dụng khi server hỗ trợ system interaction transaction; hãy xác nhận trong CapabilityStatement trước khi dùng.
fullUrl dạng urn:uuid: phù hợp cho các resource mới tham chiếu lẫn nhau trước khi có logical id. Khi xử lý transaction, server ánh xạ các URN này sang danh tính được cấp theo quy tắc transaction; client phải đọc entry.response.location thay vì tự đoán id.
{
"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" \
-H "Accept: application/fhir+json" \
-d @bundle-transaction.json
Khi transaction thành công, response là Bundle transaction-response. Với ba request POST trong ví dụ, từng entry thành công thường có response.status là 201 Created và có thể có response.location; không áp quy tắc 201 cho entry dùng method khác. Khi thất bại, server phải không để lại thay đổi từng phần, nhưng mã HTTP và cách đặt OperationOutcome cần được đọc từ response thực tế.
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 một payload minh họa được biểu diễn bằng bốn client. cURL đã có ở Bài 1; với mọi thư viện, vẫn phải kiểm tra CapabilityStatement, mã HTTP, header và OperationOutcome của server đích.
JavaScript / TypeScript (fetch thuần)
Ví dụ dùng fetch có sẵn trong các runtime JavaScript hiện đại. Nếu dự án dùng thư viện client, ghim phiên bản và kiểm tra cách thư viện xử lý response không có body.
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',
'Accept': 'application/fhir+json',
'Prefer': 'return=representation',
},
body: JSON.stringify(patient),
});
if (!res.ok) {
throw new Error(`FHIR error ${res.status}: ${await res.text()}`);
}
const responseText = await res.text();
const created = responseText ? JSON.parse(responseText) : null;
const location = res.headers.get('Location');
console.log('Created Patient:', created?.id ?? location); Python (fhirpy)
fhirpy cung cấp client Python đồng bộ và bất đồng bộ cho các interaction như CRUD và search. Kiểm tra tài liệu của phiên bản được ghim trước khi dựa vào hành vi validation hoặc xử lý lỗi. 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ị
Bảng dưới liệt kê một số lựa chọn server, client, validator và công cụ authoring. License, trạng thái bảo trì và phạm vi hỗ trợ phải được kiểm tra theo phiên bản trước khi chọn cho dự án.
| Tool | Mục đích | License |
|---|---|---|
| HAPI FHIR | Server và client Java | 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 kiểm tra triển khai
Danh sách này tập trung vào các giả định cần loại bỏ trước khi tích hợp với một endpoint FHIR cụ thể.
- Media type: gửi FHIR JSON với
Content-Type: application/fhir+jsonvà khaiAccept. Một số server chấp nhậnapplication/json, nhưng client không nên dựa vào ngoại lệ đó; nếu có 415, đọc response thực tế. - Identifier namespace: dùng nhất quán
systemvàvalue.http://fhir.hl7.org.vn/core/sid/cccdlà URI system, không phải canonical profile. - Primitive format:
birthDatedùng cú pháp FHIR như1985-03-15. Resource không parse được thường dẫn tới 400; lỗi rule/profile hoặc business rule có thể dẫn tới 422. Server không phải lúc nào cũng phân biệt chặt, vì vậy phải đọcOperationOutcome. - Reference trong transaction: relative, absolute và URN đều có vai trò theo quy tắc FHIR. Dùng
urn:uuid:cho các resource mới tham chiếu lẫn nhau trong cùng Bundle; không tuyên bố URN là dạng duy nhất hợp lệ. - Transaction entry: mỗi entry phải có
request.methodvàrequest.urlhợp lệ với method đó; đồng thời server phải công bố system interactiontransaction. - Profile resolution: trước khi gọi
$validate, xác nhận operation được hỗ trợ và server resolve được đúng VN Core package, version, dependency và terminology. - Search support: cú pháp chuẩn không bảo đảm server hỗ trợ parameter/modifier. Đối chiếu
CapabilityStatementvà test cả trường hợp có/không có kết quả. - Pagination: không tự dựng URL trang tiếp theo; theo
Bundle.link[relation="next"]do server trả. - Security metadata:
meta.securitymang security label để hệ thống chính sách diễn giải; bản thân label không thực thi phân quyền.meta.taglà metadata phân loại tổng quát, không nên dùng thay access-control policy. - Encoding: FHIR JSON dùng UTF-8; không cần
Accept-Charset. Bảo đảm client thực sự mã hóa UTF-8 và kiểm tra round-trip tiếng Việt.
9. References
- HL7 FHIR R4 spec — https://hl7.org/fhir/R4/
- FHIR RESTful API — https://hl7.org/fhir/R4/http.html
- FHIR CapabilityStatement — https://hl7.org/fhir/R4/capabilitystatement.html
- FHIR Search — https://hl7.org/fhir/R4/search.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 biên tập Omi HealthTech, reviewer kỹ thuật HungPM (Phan Mạnh Hùng), với rà soát factual về HAPI CLI semantics, identifier URI và Encounter.class CodeSystem.