FHIR RESTful API: cách FHIR dùng HTTP để trao đổi dữ liệu y tế

FHIR RESTful API là khuôn khổ tương tác hướng tài nguyên trên HTTP; khi xử lý dữ liệu y tế trong môi trường vận hành, endpoint phải được bảo vệ bằng TLS theo chính sách triển khai. JSON và XML đều là định dạng chuẩn, được thương lượng qua media type. Các tương tác mà server công bố — như đọc, tìm kiếm, tạo mới, cập nhật hoặc xóa — được ánh xạ vào phương thức HTTP và endpoint cấp instance, cấp type, cấp toàn hệ thống hoặc operation có tiền tố $.

Trang này dành cho lập trình viên web/mobile đã quen REST nhưng chưa từng code FHIR, muốn hiểu vì sao API y tế có tính chất riêng và cách tích hợp đúng chuẩn vào hệ thống bệnh viện Việt Nam theo khung pháp lý hiện hành (Luật 91/2025/QH15, NĐ 356/2025/NĐ-CP, NĐ 137/2024/NĐ-CP).

Tóm tắt nhanh

  • FHIR định nghĩa read, search, create, update, delete cùng vread, history, patch, Bundle transaction và Operation; từng server chỉ hỗ trợ tập tương tác được khai trong CapabilityStatement.
  • Định danh tài nguyên theo cú pháp [base]/[type]/[id]; cấp type cho search/create; cấp toàn hệ thống cho transaction. Bulk Data định nghĩa $export ở scope system, Patient và Group.
  • Định dạng chuẩn FHIR R4: application/fhir+jsonapplication/fhir+xml đều normative; trang này khuyến nghị JSON cho web/mobile.
  • FHIR R4 không áp đặt một cơ chế bảo mật mặc định. Triển khai phải chọn TLS, xác thực, phân quyền và kiểm toán phù hợp; SMART on FHIR là một lựa chọn chuẩn hóa phổ biến khi use case phù hợp.
  • Khung pháp lý Việt Nam đặt nghĩa vụ bảo vệ dữ liệu cá nhân và giao dịch điện tử; FHIR, SMART, AuditEventProvenance có thể hỗ trợ kiểm soát kỹ thuật nhưng không tự tạo ra tuân thủ pháp lý.

1. Cấu trúc endpoint FHIR

FHIR là REST hướng tài nguyên (resource-oriented). Mọi URL được dựng từ một service base URL kết hợp với loại tài nguyên, định danh tài nguyên, lịch sử phiên bản và truy vấn tìm kiếm. Cú pháp tổng quát theo §3.1 của FHIR R4 là:

[base]/[type]/[id]{/_history/[vid]}{?[search]}

[base] là URL gốc của server (ví dụ https://hapi.fhir.org/baseR4); [type] là tên Resource viết hoa đúng (Patient, Encounter, Observation…); [id] là chuỗi định danh logic do server cấp; [vid] là định danh phiên bản opaque do server gán, không được giả định là số tăng đơn điệu. Có bốn cấp endpoint cần phân biệt:

  • Instance level (/Patient/123): áp dụng cho read, vread, update, delete, patch.
  • Type level (/Patient): áp dụng cho search và create.
  • Whole-system level (/): dùng cho batch/transaction Bundle và search toàn server.
  • Operation ($name): áp dụng ở cả ba cấp trên, ví dụ /Patient/$match hay /Patient/123/$everything.

Ví dụ thực tế trên HAPI sandbox công khai (ghi chú: chỉ dùng cho dev/test, không gửi dữ liệu thật):

curl -H "Accept: application/fhir+json" \
  https://hapi.fhir.org/baseR4/Patient/example

curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Patient?name=Smith&_count=5"

2. Năm tương tác cốt lõi và họ hàng mở rộng

Đặc tả FHIR R4 định nghĩa các tương tác như read, search, create, update và delete, nhưng không bắt mọi server hỗ trợ cả năm cho mọi resource. Server phải công bố tập tương tác thực tế trong CapabilityStatement; bảng sau ánh xạ các tương tác phổ biến với phương thức HTTP, URL mẫu và tình huống dùng:

Tương tác HTTP URL Use case
readGET/Patient/123Đọc một Resource theo id
vreadGET/Patient/123/_history/2Đọc một phiên bản lịch sử cụ thể
searchGET/Patient?name=NguyễnTìm theo tham số
createPOST/PatientTạo mới, server tự cấp id
updatePUT/Patient/123Thay thế toàn bộ Resource
patchPATCH/Patient/123Cập nhật một phần (JSON Patch / FHIRPath Patch)
deleteDELETE/Patient/123Xóa logic tài nguyên
historyGET/Patient/123/_historyLiệt kê lịch sử thay đổi
capabilitiesGET/metadataDiscovery năng lực server

Server có quyền chọn nhóm tương tác hỗ trợ; CapabilityStatement (mục 5) liệt kê theo từng Resource. Tập tối thiểu phải được xác định theo actor và workflow trong CapabilityStatement của VN Core; không suy diễn một danh sách chung cho mọi EMR, cổng BHYT hoặc ứng dụng công dân.

3. Search — truy vấn dữ liệu lâm sàng

Search là tương tác phức tạp nhất của FHIR. Mỗi Resource có một bộ search parameter chuẩn (như name, identifier, birthdate, code, subject) cùng nhiều modifier (:exact, :contains, :missing, :not, :above, :below) và prefix cho số/ngày (eq, ne, gt, lt, ge, le).

Khi tìm theo identifier có system rõ ràng, dùng cú pháp token system|value. Trong VN Core, system định danh cá nhân (CCCD) là http://fhir.hl7.org.vn/core/sid/cccd:

curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Patient?identifier=http://fhir.hl7.org.vn/core/sid/cccd|001234567890"

curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Patient?birthdate=ge1980-01-01&birthdate=lt1990-01-01"

curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Observation?subject=Patient/example&code=http://loinc.org|2093-3"

Hai chế độ tham chiếu hữu ích là _include (kéo thêm Resource được tham chiếu từ kết quả) và _revinclude (kéo thêm Resource trỏ ngược về kết quả). Lưu ý quan trọng: cả hai là tham số tìm kiếm, chỉ áp dụng cho tương tác search ở cấp type, không áp dụng cho read instance:

# Lấy Patient kèm mọi Observation tham chiếu tới Patient đó
curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Patient?_id=example&_revinclude=Observation:subject"

# Hoặc đảo chiều: lấy Observation và kéo theo Patient liên quan
curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Observation?subject=Patient/example&_include=Observation:subject"

Chained search cho phép đi qua tham chiếu để lọc Resource cha. Ví dụ tìm Observation thuộc về Patient có họ tên chứa "Nguyễn":

curl -H "Accept: application/fhir+json" \
  "https://hapi.fhir.org/baseR4/Observation?subject:Patient.name=Nguy%E1%BB%85n"

Search không có kết quả vẫn thành công với HTTP 200 và một Bundle type=searchset không có entry khớp (thường có total = 0). HTTP 404 dùng cho tình huống như read một instance không tồn tại, không dùng cho tập kết quả search bằng không.

4. Bundle transaction — gộp nhiều thao tác nguyên tử

Bundle là Resource đặc biệt cho phép đóng gói nhiều thao tác trong một HTTP request gửi tới endpoint cấp toàn hệ thống. Hai chế độ phổ biến: type=batch (mỗi entry độc lập, lỗi cục bộ) và type=transaction (atomic — nếu một entry thất bại, toàn bộ rollback). Transaction cũng hỗ trợ tham chiếu chéo qua fullUrl dạng urn:uuid: và header ifNoneExist cho conditional create.

Ví dụ chạy được trên HAPI sandbox (lưu vào bundle.json, gửi bằng curl --data-binary @bundle.json):

POST / HTTP/1.1
Host: hapi.fhir.org
Content-Type: application/fhir+json
Accept: application/fhir+json

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:patient-001",
      "resource": {
        "resourceType": "Patient",
        "identifier": [{
          "system": "http://fhir.hl7.org.vn/core/sid/cccd",
          "value": "001234567890"
        }],
        "name": [{ "family": "Nguyễn", "given": ["Văn An"] }],
        "gender": "male",
        "birthDate": "1985-04-12"
      },
      "request": {
        "method": "POST",
        "url": "Patient",
        "ifNoneExist": "identifier=http://fhir.hl7.org.vn/core/sid/cccd|001234567890"
      }
    },
    {
      "resource": {
        "resourceType": "Encounter",
        "status": "in-progress",
        "class": {
          "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
          "code": "AMB",
          "display": "Ambulatory"
        },
        "subject": { "reference": "urn:uuid:patient-001" }
      },
      "request": { "method": "POST", "url": "Encounter" }
    }
  ]
}

Server trả về Bundle type=transaction-response, mỗi entry mang response.status (ví dụ 201 Created) và response.location chỉ tới Resource vừa được tạo. Đây là cách thanh lịch để đẩy một ca khám trọn gói (Patient + Encounter + Condition + Observation) lên FHIR server từ HIS.

5. CapabilityStatement — discovery của server

Bất kỳ FHIR server nào cũng phải đáp lời tại GET [base]/metadata bằng một CapabilityStatement, mô tả: phiên bản FHIR (4.0.1 cho R4), các Resource hỗ trợ, danh sách tương tác cho từng Resource, search parameter đã implement, các Operation và phương thức bảo mật. Đây là tài liệu máy đọc được giúp client biết "server này làm được gì" trước khi gọi tiếp.

curl -H "Accept: application/fhir+json" \
  https://hapi.fhir.org/baseR4/metadata | jq '.fhirVersion, .rest[0].resource[].type'

Một trích đoạn CapabilityStatement tối giản cho VN Core:

{
  "resourceType": "CapabilityStatement",
  "status": "active",
  "date": "2026-05-02",
  "kind": "instance",
  "fhirVersion": "4.0.1",
  "format": ["application/fhir+json", "application/fhir+xml"],
  "rest": [{
    "mode": "server",
    "security": {
      "service": [{
        "coding": [{
          "system": "http://terminology.hl7.org/CodeSystem/restful-security-service",
          "code": "SMART-on-FHIR"
        }]
      }]
    },
    "resource": [{
      "type": "Patient",
      "interaction": [
        { "code": "read" },
        { "code": "search-type" },
        { "code": "create" },
        { "code": "update" }
      ],
      "searchParam": [
        { "name": "identifier", "type": "token" },
        { "name": "name", "type": "string" },
        { "name": "birthdate", "type": "date" }
      ]
    }]
  }]
}

Khi triển khai cho bệnh viện, nên publish CapabilityStatement riêng cho production và staging, nêu rõ profile VN Core đang ràng buộc và scope SMART on FHIR đang hỗ trợ. Đây cũng là tạo thuận lợi cho audit và đánh giá theo Luật 91/2025/QH15 về Bảo vệ dữ liệu cá nhân.

6. Operations: $expand, $validate, $everything

FHIR Operation là lời gọi có tên bắt đầu bằng $, được định nghĩa trong base specification hoặc một IG/endpoint. Cách gọi GET hay POST, tham số và phạm vi hỗ trợ phải theo OperationDefinition cùng CapabilityStatement áp dụng. Một số operation thường gặp:

  • $validate — kiểm tra Resource có hợp lệ với một profile không, trước khi POST chính thức.
  • $expand — mở rộng ValueSet thành danh sách code (dùng cho dropdown UI).
  • $lookup — tra một code trong CodeSystem, lấy display, designation, tính chất.
  • $everything — gọi /Patient/123/$everything để yêu cầu tập thông tin liên quan theo phạm vi server triển khai; không giả định đây là toàn bộ hồ sơ pháp lý hay dữ liệu ở mọi hệ thống.
  • $translate — ánh xạ code qua ConceptMap đã công bố, có hướng, version và equivalence rõ ràng.

Validate trước khi commit là thói quen quan trọng cho hệ thống Việt Nam, nơi nhiều profile VN Core đặt ràng buộc Must Support trên CCCD, dân tộc và phường/xã:

POST /Patient/$validate?profile=http://fhir.hl7.org.vn/core/StructureDefinition/vn-core-patient HTTP/1.1
Host: hapi.fhir.org
Content-Type: application/fhir+json

{
  "resourceType": "Patient",
  "identifier": [{
    "system": "http://fhir.hl7.org.vn/core/sid/cccd",
    "value": "001234567890"
  }],
  "name": [{ "family": "Trần", "given": ["Thị Bình"] }],
  "gender": "female",
  "birthDate": "1990-08-21"
}

7. Bulk Data API và $export bất đồng bộ

Khi cần xuất dữ liệu khối lớn (cohort cho nghiên cứu, huấn luyện AI y tế, đối soát BHYT), $export là một lựa chọn bất đồng bộ được chuẩn hóa khi endpoint công bố hỗ trợ đúng phiên bản Bulk Data Access IG, scope ủy quyền và tham số áp dụng. Quy trình có ba bước:

  1. Client gửi yêu cầu kèm header Prefer: respond-async; server đáp 202 Accepted kèm header Content-Location trỏ tới URL polling.
  2. Client poll URL đó; khi xong, server trả manifest JSON liệt kê các tệp NDJSON theo từng loại Resource.
  3. Client tải từng tệp NDJSON, mỗi dòng là một Resource độc lập.

Bulk Data IG định nghĩa ba scope khởi tạo export:

  • System-level: GET [base]/$export.
  • Patient-level: GET [base]/Patient/$export cho tập bệnh nhân mà client được phép truy cập.
  • Group-level: GET [base]/Group/[id]/$export cho thành viên của Group.

Yêu cầu minh họa cho Group-level export:

GET /Group/vn-pilot-cohort-2026/$export?_type=Patient,Observation,Condition&_since=2026-01-01T00:00:00Z HTTP/1.1
Host: hapi.fhir.org
Accept: application/fhir+json
Prefer: respond-async
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR...

Lưu ý pháp lý

Dữ liệu y tế là dữ liệu cá nhân nhạy cảm. Trước mỗi luồng $export, xác định vai trò xử lý, mục đích, phạm vi dữ liệu, người nhận và căn cứ xử lý. Nghĩa vụ lập/cập nhật hồ sơ đánh giá tác động, Mẫu số 09 cho chuyển dữ liệu xuyên biên giới và phạm vi AuditEvent phải được xác định theo điều kiện áp dụng thực tế, data flow, threat model và chính sách kiểm toán; không mặc định mọi export đều có cùng bộ hồ sơ hoặc phải ghi mọi sự kiện giống nhau.

8. Xác thực: SMART on FHIR + OAuth 2.0

SMART App Launch là profile của OAuth 2.0 và OpenID Connect dành cho ứng dụng FHIR, với hai ngữ cảnh launch chính:

  • EHR launch — ứng dụng được EMR mở trong ngữ cảnh bệnh nhân/bác sĩ đang đăng nhập.
  • Standalone launch — ứng dụng (ví dụ portal cho bệnh nhân) khởi chạy độc lập, người dùng đăng nhập trực tiếp.

Backend Services là profile ủy quyền máy-với-máy riêng trong hệ sinh thái SMART, thường dùng JWT client assertion và scope system/...; đây không phải một launch mode có ngữ cảnh người dùng.

Scope tuân theo cú pháp <ngữ-cảnh>/<Resource>.<quyền>, ví dụ:

patient/Patient.read       # đọc hồ sơ của bệnh nhân hiện tại
patient/Observation.rs     # read + search Observation
user/Encounter.cruds       # mọi quyền với Encounter
system/*.read              # backend đọc tất cả

Trong luồng authorization code có PKCE, client gọi /authorize, người dùng xác thực và cấp quyền cho các scope được trình bày; server trả mã ủy quyền, rồi client đổi mã lấy access token tại /token. OAuth grant này không tự thay thế một quyết định đồng ý pháp lý hoặc tài nguyên FHIR Consent:

POST /oauth2/token HTTP/1.1
Host: auth.example-ehr.vn
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=Aa1Bb2Cc3...&
redirect_uri=https%3A%2F%2Fapp.omihealth.vn%2Fcallback&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk&
client_id=app-omihealth-vn

Access token có thể là JWT hoặc chuỗi opaque; thời hạn và việc cấp refresh token phụ thuộc authorization server và loại client. Với endpoint được bảo vệ theo SMART/OAuth, request dùng Authorization: Bearer <token> qua TLS. Client đọc metadata discovery tại .well-known/smart-configuration thay vì giả định cấu hình token.

9. Mã trạng thái và OperationOutcome

FHIR R4 quy định khá chặt cách dùng HTTP status code:

  • 200 OK — read/search/update thành công.
  • 201 Created — create thành công, kèm header LocationETag.
  • 204 No Content — delete thành công không trả body.
  • 304 Not Modified — phản hồi cho conditional read khi dữ liệu chưa đổi.
  • 400 Bad Request — Resource không hợp lệ về cú pháp.
  • 401 Unauthorized / 403 Forbidden — lỗi xác thực hoặc phân quyền theo security contract; nguyên nhân không nhất thiết chỉ là access token.
  • 404 Not Found — instance được read không tồn tại; search không khớp vẫn trả Bundle rỗng với 200.
  • 409 Conflict — xung đột trạng thái mà interaction hoặc hợp đồng API quy định; không dùng thay cho If-Match thất bại.
  • 410 Gone — Resource đã bị delete logic.
  • 412 Precondition Failed — điều kiện không khớp, bao gồm If-Match thất bại trong version-aware update.
  • 422 Unprocessable Entity — một số server dùng cho lỗi profile hoặc quy tắc nghiệp vụ; client không được giả định mọi invariant đều trả mã này.
  • 5xx — lỗi phía server.

FHIR khuyến nghị (SHOULD) trả OperationOutcome trong body cho các lỗi cấp FHIR (4xx/5xx liên quan tới ngữ nghĩa Resource). Tuy nhiên client phải chấp nhận trường hợp body rỗng hoặc không phải FHIR ở tầng hạ tầng (ví dụ proxy trả về HTML). Một OperationOutcome điển hình:

{
  "resourceType": "OperationOutcome",
  "issue": [{
    "severity": "error",
    "code": "invariant",
    "details": {
      "text": "Patient.identifier yêu cầu hệ thống CCCD theo VNCorePatient profile"
    },
    "diagnostics": "Identifier with system=http://fhir.hl7.org.vn/core/sid/cccd is required.",
    "expression": ["Patient.identifier"]
  }]
}

Invariant trong VNCorePatient xác định điều kiện hợp lệ, không tự quy định một mã HTTP duy nhất. Tùy interaction và hợp đồng API, server có thể dùng 400 hoặc 422 và nên trả OperationOutcome đủ chi tiết khi phù hợp; client không được cam kết cứng rằng mọi vi phạm đều có issue.code = "invariant" hoặc một expression cụ thể.

10. Versioning, conditional, paging

FHIR chuẩn hóa ba cơ chế thường cần thống nhất rõ trong hợp đồng API:

  • Optimistic concurrency — khi server công bố chính sách versioned-update, client gửi If-Match: W/"2"; phiên bản không khớp được xử lý bằng HTTP 412 theo đặc tả.
  • Conditional create / update / delete — khi server công bố hỗ trợ, dùng header If-None-Exist hoặc điều kiện tìm kiếm để tránh tạo trùng trong các luồng ingest.
  • Paging — search trả Bundle type=searchset với link do server cung cấp; client đi theo link.relation = "next" thay vì tự dựng URL. _count là đề nghị kích thước trang và server có thể điều chỉnh hoặc bỏ qua.
curl -X PUT \
  -H "Content-Type: application/fhir+json" \
  -H "If-Match: W/\"2\"" \
  --data-binary @patient-v3.json \
  https://hapi.fhir.org/baseR4/Patient/123

curl -X POST \
  -H "Content-Type: application/fhir+json" \
  -H "If-None-Exist: identifier=http://fhir.hl7.org.vn/core/sid/cccd|001234567890" \
  --data-binary @patient-new.json \
  https://hapi.fhir.org/baseR4/Patient

11. Thực hành triển khai trong bối cảnh Việt Nam

FHIR là tiêu chuẩn kỹ thuật trung lập, nhưng triển khai thực tế phải bám sát khung pháp lý. Tách bạch giữa nghĩa vụ pháp lý và lựa chọn kỹ thuật để hiện thực hóa:

  • Bảo vệ dữ liệu cá nhân: xác định nghĩa vụ theo vai trò, mục đích và rủi ro xử lý; dùng TLS, mã hóa lưu trữ, phân quyền chi tiết và nhật ký kiểm toán theo chính sách đã phê duyệt. SMART scope, AuditEventConsent là các cơ chế hỗ trợ, không thay thế DPIA, quản trị truy cập hoặc căn cứ xử lý.
  • Giao dịch điện tử và chữ ký số: khi văn bản hoặc quy trình nghiệp vụ yêu cầu chữ ký điện tử, chọn phạm vi ký và cơ chế xác minh phù hợp. FHIR có thể mang chữ ký qua Provenance.signature hoặc Bundle.signature; tính hợp lệ pháp lý còn phụ thuộc chứng thư, chính sách ký và dữ liệu canonical thực sự được ký.
  • Bệnh án điện tử: TT 13/2025/TT-BYT đặt các mốc khác nhau cho bệnh viện và cơ sở KCB khác. FHIR REST + VN Core là một lựa chọn kỹ thuật để thí điểm liên thông; Thông tư không mặc định quy định VN Core là cơ chế duy nhất.
  • Hiệu năng và độ ổn định: xác định kích thước trang, rate limit và cache bằng kiểm thử tải cùng SLO của từng actor. Công bố giới hạn trong tài liệu API; dùng ETag theo chính sách versioning mà server khai báo.
  • Kiểm thử: dùng HAPI sandbox công khai cho dev/test, không bao giờ gửi dữ liệu thật; chạy $validate trong CI/CD trước khi merge profile mới.

Khuyến nghị từ Omi HealthTech

Khi tích hợp FHIR vào HIS Việt Nam, có thể tách tầng FHIR façade khỏi nghiệp vụ phía sau để quản lý hợp đồng API và kiểm soát truy cập nhất quán. Tuân thủ vẫn là trách nhiệm xuyên suốt kiến trúc, dữ liệu, con người và vận hành; không nằm riêng ở façade. Hỏi đáp chi tiết, liên hệ [email protected].

Tham chiếu