Chuyển đến nội dung chính

Bài 19: Hands-on — FHIR Client và Tích hợp Ứng dụng

HAPI FHIR Client (Java), fhir.js (JavaScript), Python fhirclient, Fluent Client API, Generic Client, xây dựng ứng dụng quản lý bệnh nhân với React + FHIR, mobile app integration, error handling và retry patterns.

🏗️ Kiến trúc — Bài 19 Bài 19: Hands-on — FHIR Client và Tích hợp Ứng dụng

HL7 FHIR - Chuẩn Dữ liệu Y tế từ Cơ bản đến Nâng cao

Phần 6: Thực hành - Xây dựng hệ thống FHIR

xdev.asia

Xem bản video

1. FHIR Client Overview

FHIR Client là thư viện giúp ứng dụng giao tiếp với FHIR Server qua RESTful API. Mỗi ngôn ngữ có thư viện riêng.

Ngôn ngữThư việnĐặc điểm
JavaHAPI FHIR ClientĐầy đủ nhất, Generic + Fluent API
JavaScriptfhirclient.js / fhir.jsBrowser + Node.js, SMART on FHIR
PythonfhirclientSMART on FHIR, Pydantic models
.NETFirely SDK (Hl7.Fhir)NuGet package, strong typing
SwiftSwift-FHIRiOS/macOS native

2. HAPI FHIR Client (Java)

Generic Client

FhirContext ctx = FhirContext.forR5();
IGenericClient client = ctx.newRestfulGenericClient(
    "http://localhost:8080/fhir");

// READ
Patient patient = client.read()
    .resource(Patient.class)
    .withId("patient-001")
    .execute();

System.out.println("Bệnh nhân: " + 
    patient.getNameFirstRep().getNameAsSingleString());

// CREATE
Patient newPatient = new Patient();
newPatient.addName()
    .setFamily("Nguyễn")
    .addGiven("Văn A");
newPatient.addIdentifier()
    .setSystem("http://xdev.asia/fhir/sid/cccd")
    .setValue("001234567890");
newPatient.setGender(Enumerations.AdministrativeGender.MALE);
newPatient.setBirthDateElement(new DateType("1990-05-15"));

MethodOutcome outcome = client.create()
    .resource(newPatient)
    .execute();

System.out.println("Created ID: " + 
    outcome.getId().getIdPart());

// SEARCH
Bundle results = client.search()
    .forResource(Patient.class)
    .where(Patient.NAME.matches().value("Nguyễn"))
    .and(Patient.BIRTHDATE.afterOrEquals().day("1990-01-01"))
    .returnBundle(Bundle.class)
    .execute();

for (Bundle.BundleEntryComponent entry : results.getEntry()) {
    Patient p = (Patient) entry.getResource();
    System.out.println(p.getNameFirstRep().getNameAsSingleString());
}

Fluent Client — Annotation-based

// Định nghĩa interface
@FhirClient
public interface PatientClient {
    
    @Read
    Patient readPatient(@IdParam IdType id);
    
    @Search
    Bundle searchByName(
        @RequiredParam(name = Patient.SP_NAME) StringParam name);
    
    @Create
    MethodOutcome createPatient(@ResourceParam Patient patient);
    
    @Update
    MethodOutcome updatePatient(
        @IdParam IdType id, 
        @ResourceParam Patient patient);
    
    @Delete
    void deletePatient(@IdParam IdType id);
}

// Sử dụng
PatientClient patientClient = ctx.newRestfulClient(
    PatientClient.class, "http://localhost:8080/fhir");
Patient patient = patientClient.readPatient(new IdType("patient-001"));

3. JavaScript FHIR Client (fhirclient.js)

npm install fhirclient
import FHIR from 'fhirclient';

// Kết nối FHIR Server (standalone launch)
const client = FHIR.client({
  serverUrl: 'http://localhost:8080/fhir'
});

// Read Patient
const patient = await client.request('Patient/patient-001');
console.log(`Bệnh nhân: ${patient.name[0].family} ${patient.name[0].given[0]}`);

// Search
const bundle = await client.request(
  'Patient?name=Nguyen&birthdate=ge1990-01-01&_count=20'
);
bundle.entry?.forEach(entry => {
  console.log(entry.resource.name[0].text);
});

// Create
const newPatient = {
  resourceType: 'Patient',
  name: [{ family: 'Trần', given: ['Thị B'] }],
  gender: 'female',
  birthDate: '1995-08-20',
  identifier: [{
    system: 'http://xdev.asia/fhir/sid/cccd',
    value: '001987654321'
  }]
};

const created = await client.create(newPatient);
console.log('Created:', created.id);

SMART on FHIR Launch

// launch.html — redirect page
FHIR.oauth2.authorize({
  clientId: 'my-fhir-app',
  scope: 'patient/*.read user/Patient.read launch/patient',
  redirectUri: '/callback'
});

// callback.html — sau khi authorize
FHIR.oauth2.ready()
  .then(client => {
    // Tự động lấy Patient context
    return client.patient.read();
  })
  .then(patient => {
    console.log('Current patient:', patient.name[0].text);
    
    // Lấy Observations của patient hiện tại
    return client.request(
      `Observation?patient=${patient.id}&code=8867-4&_sort=-date&_count=10`
    );
  })
  .then(bundle => {
    bundle.entry?.forEach(entry => {
      const obs = entry.resource;
      console.log(`${obs.code.text}: ${obs.valueQuantity.value} ${obs.valueQuantity.unit}`);
    });
  });

4. Python FHIR Client

pip install fhirclient
from fhirclient import client
from fhirclient.models.patient import Patient
from fhirclient.models.observation import Observation

# Cấu hình
settings = {
    'app_id': 'my_fhir_app',
    'api_base': 'http://localhost:8080/fhir'
}
smart = client.FHIRClient(settings=settings)

# Read Patient
patient = Patient.read('patient-001', smart.server)
print(f"Bệnh nhân: {patient.name[0].family} {patient.name[0].given[0]}")

# Search
search = Patient.where(struct={
    'name': 'Nguyen',
    'birthdate': 'ge1990-01-01'
})
patients = search.perform_resources(smart.server)
for p in patients:
    print(f"  - {p.name[0].text} ({p.birthDate.isostring})")

# Create Observation
obs = Observation()
obs.status = 'final'
obs.code = {
    'coding': [{
        'system': 'http://loinc.org',
        'code': '8867-4',
        'display': 'Heart rate'
    }]
}
obs.subject = {'reference': f'Patient/patient-001'}
obs.valueQuantity = {
    'value': 72,
    'unit': 'beats/minute',
    'system': 'http://unitsofmeasure.org',
    'code': '/min'
}
result = obs.create(smart.server)
print(f"Created Observation: {result}")

5. Xây dựng ứng dụng Quản lý BN với React

// PatientList.tsx
import { useState, useEffect } from 'react';
import FHIR from 'fhirclient';

interface FHIRPatient {
  id: string;
  name: Array<{ family: string; given: string[] }>;
  gender: string;
  birthDate: string;
}

export function PatientList() {
  const [patients, setPatients] = useState<FHIRPatient[]>([]);
  const [search, setSearch] = useState('');
  const client = FHIR.client({ serverUrl: '/fhir' });

  useEffect(() => {
    loadPatients();
  }, [search]);

  async function loadPatients() {
    const params = search 
      ? `Patient?name=${encodeURIComponent(search)}&_count=20`
      : 'Patient?_count=20&_sort=-_lastUpdated';
    
    const bundle = await client.request(params);
    const list = bundle.entry?.map(
      (e: any) => e.resource as FHIRPatient
    ) || [];
    setPatients(list);
  }

  return (
    <div>
      <input 
        placeholder="Tìm bệnh nhân..."
        value={search}
        onChange={e => setSearch(e.target.value)}
      />
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>Họ tên</th>
            <th>Giới tính</th>
            <th>Ngày sinh</th>
          </tr>
        </thead>
        <tbody>
          {patients.map(p => (
            <tr key={p.id}>
              <td>{p.id}</td>
              <td>{p.name[0]?.family} {p.name[0]?.given?.join(' ')}</td>
              <td>{p.gender}</td>
              <td>{p.birthDate}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

6. Error Handling và Retry Patterns

// Java - Retry với exponential backoff
public <T extends IBaseResource> T readWithRetry(
        Class<T> resourceClass, String id, int maxRetries) {
    
    int attempt = 0;
    while (attempt < maxRetries) {
        try {
            return client.read()
                .resource(resourceClass)
                .withId(id)
                .execute();
        } catch (ResourceNotFoundException e) {
            throw e; // 404 — không retry
        } catch (FhirClientConnectionException e) {
            attempt++;
            if (attempt == maxRetries) throw e;
            
            long delay = (long) Math.pow(2, attempt) * 1000;
            Thread.sleep(delay);
        }
    }
    throw new IllegalStateException("Max retries exceeded");
}
// OperationOutcome handling
try {
    client.create().resource(patient).execute();
} catch (UnprocessableEntityException e) {
    OperationOutcome oo = (OperationOutcome) e.getOperationOutcome();
    for (var issue : oo.getIssue()) {
        System.err.println(String.format("[%s] %s: %s",
            issue.getSeverity(),
            issue.getLocation(),
            issue.getDiagnostics()));
    }
}

7. Tổng kết

  • HAPI FHIR Client (Java) — Generic Client cho flexibility, Fluent Client cho type-safety

  • fhirclient.js — SMART on FHIR launch, browser + Node.js

  • Python fhirclient — Pydantic models, dễ tích hợp data pipeline

  • React + FHIR — SPA quản lý bệnh nhân kết nối trực tiếp FHIR Server

  • Error handling — OperationOutcome parsing, retry với exponential backoff