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

Lesson 19: Hands-on — FHIR Client and Application Integration

HAPI FHIR Client (Java), fhir.js (JavaScript), Python fhirclient, Fluent Client API, Generic Client, build patient management application with React + FHIR, mobile app integration, error handling and retry patterns.

🏗️ Architecture — Lesson 19 Lesson 19: Hands-on — FHIR Client and Integration Application

HL7 FHIR - Basic to Advanced Healthcare Data Standard

Part 6: Practice - Building the FHIR system

xdev.asia

1. FHIR Client Overview

FHIR Client is a library that helps applications communicate with FHIR Server via RESTful API. Each language has its own library.

LanguageLibraryCharacteristics
JavaHAPI FHIR ClientMost complete, 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. Build a Patient Management application with 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 and 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. Summary

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

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

  • Python fhirclient — Pydantic models, easy to integrate data pipeline

  • React + FHIR — Patient management SPA connects directly to FHIR Server

  • Error handling — OperationOutcome parsing, retry with exponential backoff