สรุป
HL7 FHIR (Fast Healthcare Interoperability Resources) เป็นมาตรฐานสมัยใหม่สำหรับการแลกเปลี่ยนข้อมูลสุขภาพแบบอิเล็กทรอนิกส์ โดยใช้ RESTful API รับ-ส่งข้อมูลแบบ JSON/XML ครอบคลุมทรัพยากรที่สำคัญ เช่น ผู้ป่วย ผลสังเกตการณ์ ยา พร้อมวิธีการยืนยันตัวตนด้วย OAuth 2.0 และ SMART on FHIR บทความนี้เน้นโครงสร้าง FHIR ประเภททรัพยากร วิธีค้นหา การยืนยันตัวตน และเทคนิคการนำไปใช้งานจริงอย่างเป็นขั้นตอน
บทนำ
ข้อมูลสุขภาพที่กระจัดกระจายทำให้ระบบสุขภาพสหรัฐฯ สูญเสียต้นทุนปีละ 30 พันล้านดอลลาร์ นักพัฒนาที่สร้างแอปสุขภาพควรรวม HL7 FHIR API ซึ่งเป็นมาตรฐานตามข้อกำหนด CMS และผู้ให้บริการ EHR รายใหญ่ เช่น Epic, Cerner
ข้อเท็จจริง: การใช้งาน FHIR ลดเวลาประสานงานดูแล 40% และลดการขอเวชระเบียนผ่านแฟกซ์ได้ถึง 85% FHIR API ที่แข็งแกร่งช่วยให้ EHR, patient portal และระบบประสานงานดูแล เชื่อมต่อข้อมูลได้คล่องตัว
ไกด์นี้จะพาคุณทีละขั้นตอนตั้งแต่สถาปัตยกรรม FHIR, ประเภท resource, การค้นหา, OAuth 2.0, SMART on FHIR ไปจนถึงการนำไปใช้จริงใน production
💡 Apidog ช่วยให้รวม API สุขภาพง่ายขึ้น: ทดสอบ FHIR endpoint, ตรวจ schema, แก้ไข auth flow, ทำเอกสาร API, นำเข้า Implementation Guide, จำลอง response, แชร์ test case กับทีม
HL7 FHIR คืออะไร?
FHIR (Fast Healthcare Interoperability Resources) คือกรอบงานมาตรฐานการแลกเปลี่ยนข้อมูลสุขภาพทางอิเล็กทรอนิกส์ พัฒนาโดย HL7 ใช้ RESTful API, JSON, XML, OAuth 2.0
ประเภททรัพยากร FHIR
FHIR มีทรัพยากรกว่า 140 แบบ ที่ใช้บ่อย ได้แก่:
| ทรัพยากร (Resource) | วัตถุประสงค์ (Purpose) | กรณีการใช้งานทั่วไป (Common Use Cases) |
|---|---|---|
| Patient | ข้อมูลประชากร | การค้นหาผู้ป่วย, การลงทะเบียน |
| Practitioner | ข้อมูลผู้ให้บริการ | ไดเรกทอรี, การจัดตารางเวลา |
| Encounter | การเข้าพบ/การรับเข้า | ตอนการดูแล, การเรียกเก็บเงิน |
| Observation | ข้อมูลทางคลินิก | สัญญาณชีพ, ผลตรวจทางห้องปฏิบัติการ, การประเมิน |
| Condition | ปัญหา/การวินิจฉัย | รายการปัญหา, การวางแผนการดูแล |
| MedicationRequest | ใบสั่งยา | การสั่งยาทางอิเล็กทรอนิกส์, ประวัติการใช้ยา |
| AllergyIntolerance | อาการแพ้ | การตรวจสอบความปลอดภัย, การแจ้งเตือน |
| Immunization | การฉีดวัคซีน | ประวัติการฉีดวัคซีน |
| DiagnosticReport | รายงานห้องปฏิบัติการ/ภาพวินิจฉัย | การส่งผลลัพธ์ |
| DocumentReference | เอกสารทางคลินิก | CCD, สรุปการจำหน่าย |
สถาปัตยกรรม FHIR API
FHIR ใช้ RESTful API URL แบบนี้
https://fhir-server.com/fhir/{resourceType}/{id}
เปรียบเทียบเวอร์ชัน FHIR
| เวอร์ชัน (Version) | สถานะ (Status) | กรณีการใช้งาน (Use Case) |
|---|---|---|
| R4 (4.0.1) | STU ปัจจุบัน | การนำไปใช้งานจริง |
| R4B (4.3) | Trial Implementation | ผู้ใช้เริ่มต้น |
| R5 (5.0.0) | Draft STU | การนำไปใช้งานในอนาคต |
| DSTU2 | เลิกใช้แล้ว | เฉพาะระบบเดิมเท่านั้น |
CMS กำหนดให้ EHR ที่รับรอง ต้องรองรับ FHIR R4 สำหรับ Patient/Provider Access APIs
เริ่มต้นใช้งาน: การเข้าถึงเซิร์ฟเวอร์ FHIR
ขั้นตอนที่ 1: เลือกเซิร์ฟเวอร์ FHIR
ตัวเลือกเซิร์ฟเวอร์ (managed, self-hosted, commercial):
| เซิร์ฟเวอร์ (Server) | ประเภท (Type) | ค่าใช้จ่าย (Cost) | เหมาะที่สุดสำหรับ (Best For) |
|---|---|---|---|
| Azure API for FHIR | Managed | จ่ายตามการใช้งาน | ระดับองค์กร, ลูกค้า Azure |
| AWS HealthLake | Managed | จ่ายตามการใช้งาน | สภาพแวดล้อม AWS |
| Google Cloud Healthcare API | Managed | จ่ายตามการใช้งาน | สภาพแวดล้อม GCP |
| HAPI FHIR | โอเพนซอร์ส | โฮสต์เอง | การปรับใช้แบบกำหนดเอง |
| Epic FHIR Server | เชิงพาณิชย์ | ลูกค้า Epic | การผสานรวม Epic EHR |
| Cerner Ignite FHIR | เชิงพาณิชย์ | ลูกค้า Cerner | การผสานรวม Cerner EHR |
ขั้นตอนที่ 2: รับข้อมูลรับรองเซิร์ฟเวอร์
ตัวอย่างการตั้งค่าบนคลาวด์:
# Azure API for FHIR
# 1. สร้างบริการ FHIR ใน Azure Portal
# 2. กำหนดค่าการตรวจสอบสิทธิ์ (OAuth 2.0 หรือ AAD)
# 3. รับ FHIR endpoint: https://{service-name}.azurehealthcareapis.com
# 4. ลงทะเบียนแอปไคลเอนต์สำหรับ OAuth
# AWS HealthLake
# 1. สร้าง Data Store ใน AWS Console
# 2. กำหนดค่าบทบาท IAM
# 3. รับ endpoint: https://healthlake.{region}.amazonaws.com
ขั้นตอนที่ 3: เข้าใจ RESTful methods ของ FHIR
| การดำเนินการ (Operation) | เมธอด HTTP (HTTP Method) | Endpoint | คำอธิบาย (Description) |
|---|---|---|---|
| Read | GET | /{resourceType}/{id} |
รับทรัพยากรที่เจาะจง |
| Search | GET | /{resourceType}?param=value |
ค้นหาทรัพยากร |
| Create | POST | /{resourceType} |
สร้างทรัพยากรใหม่ |
| Update | PUT | /{resourceType}/{id} |
แทนที่ทรัพยากร |
| Patch | PATCH | /{resourceType}/{id} |
อัปเดตบางส่วน |
| Delete | DELETE | /{resourceType}/{id} |
ลบทรัพยากร |
| History | GET | /{resourceType}/{id}/_history |
เวอร์ชันของทรัพยากร |
ขั้นตอนที่ 4: ทดสอบเรียกใช้งาน FHIR
เชื่อมต่อและดึง CapabilityStatement:
curl -X GET "https://fhir-server.com/fhir/metadata" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer {token}"
ตัวอย่าง response:
{
"resourceType": "CapabilityStatement",
"status": "active",
"date": "2026-03-25",
"fhirVersion": "4.0.1",
"rest": [{
"mode": "server",
"resource": [
{ "type": "Patient" },
{ "type": "Observation" },
{ "type": "Condition" }
]
}]
}
การดำเนินการหลักของ FHIR
การอ่านทรัพยากรผู้ป่วย
ดึงข้อมูลผู้ป่วยแบบ programmatic:
const FHIR_BASE_URL = process.env.FHIR_BASE_URL;
const FHIR_TOKEN = process.env.FHIR_TOKEN;
const fhirRequest = async (endpoint, options = {}) => {
const response = await fetch(`${FHIR_BASE_URL}/fhir${endpoint}`, {
...options,
headers: {
'Accept': 'application/fhir+json',
'Authorization': `Bearer ${FHIR_TOKEN}`,
...options.headers
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(`FHIR Error: ${error.issue?.[0]?.details?.text || response.statusText}`);
}
return response.json();
};
// อ่านข้อมูลผู้ป่วยด้วย ID
const getPatient = async (patientId) => {
const patient = await fhirRequest(`/Patient/${patientId}`);
return patient;
};
// การใช้งาน
const patient = await getPatient('12345');
console.log(`ผู้ป่วย: ${patient.name[0].given[0]} ${patient.name[0].family}`);
console.log(`วันเกิด: ${patient.birthDate}`);
console.log(`เพศ: ${patient.gender}`);
โครงสร้างทรัพยากรผู้ป่วย
{
"resourceType": "Patient",
"id": "12345",
"identifier": [
{
"use": "usual",
"type": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR"
}]
},
"system": "http://hospital.example.org",
"value": "MRN123456"
}
],
"name": [
{
"use": "official",
"family": "Smith",
"given": ["John", "Michael"]
}
],
"telecom": [
{
"system": "phone",
"value": "555-123-4567",
"use": "home"
},
{
"system": "email",
"value": "john.smith@email.com"
}
],
"gender": "male",
"birthDate": "1985-06-15",
"address": [
{
"use": "home",
"line": ["123 Main Street"],
"city": "Springfield",
"state": "IL",
"postalCode": "62701"
}
]
}
การค้นหาทรัพยากร
ค้นหาผู้ป่วยด้วยชื่อ/วันเกิด:
const searchPatients = async (searchParams) => {
const query = new URLSearchParams();
if (searchParams.name) {
query.append('name', searchParams.name);
}
if (searchParams.birthDate) {
query.append('birthdate', searchParams.birthDate);
}
if (searchParams.identifier) {
query.append('identifier', searchParams.identifier);
}
if (searchParams.gender) {
query.append('gender', searchParams.gender);
}
const response = await fhirRequest(`/Patient?${query.toString()}`);
return response;
};
// การใช้งาน
const results = await searchPatients({ name: 'Smith', birthDate: '1985-06-15' });
console.log(`พบผู้ป่วย ${results.total} ราย`);
results.entry.forEach(entry => {
const patient = entry.resource;
console.log(`${patient.name[0].family}, ${patient.name[0].given[0]}`);
});
พารามิเตอร์การค้นหาที่พบบ่อย
| ทรัพยากร (Resource) | พารามิเตอร์การค้นหา (Search Parameters) | ตัวอย่าง (Example) |
|---|---|---|
| Patient | name, birthdate, identifier, gender, phone, email | ?name=Smith&birthdate=1985-06-15 |
| Observation | patient, code, date, category, status | ?patient=123&code=8480-6&date=ge2026-01-01 |
| Condition | patient, clinical-status, category, onset-date | ?patient=123&clinical-status=active |
| MedicationRequest | patient, status, intent, medication | ?patient=123&status=active |
| Encounter | patient, date, status, class | ?patient=123&date=ge2026-01-01 |
| DiagnosticReport | patient, category, date, status | ?patient=123&category=laboratory |
ตัวปรับแต่งการค้นหา
| ตัวปรับแต่ง (Modifier) | คำอธิบาย (Description) | ตัวอย่าง (Example) |
|---|---|---|
:exact |
ตรงกันทุกประการ | name:exact=Smith |
:contains |
มีส่วนประกอบ | name:contains=smi |
:missing |
มี/ไม่มีค่า | phone:missing=true |
: (คำนำหน้า) |
ตัวดำเนินการคำนำหน้า | birthdate=ge1980-01-01 |
คำนำหน้าการค้นหาสำหรับวันที่และตัวเลข
| คำนำหน้า (Prefix) | ความหมาย (Meaning) | ตัวอย่าง (Example) |
|---|---|---|
eq |
เท่ากับ | birthdate=eq1985-06-15 |
ne |
ไม่เท่ากับ | birthdate=ne1985-06-15 |
gt |
มากกว่า | birthdate=gt1980-01-01 |
lt |
น้อยกว่า | birthdate=lt1990-01-01 |
ge |
มากกว่าหรือเท่ากับ | birthdate=ge1980-01-01 |
le |
น้อยกว่าหรือเท่ากับ | birthdate=le1990-01-01 |
sa |
เริ่มต้นหลังจาก | date=sa2026-01-01 |
eb |
สิ้นสุดก่อน | date=eb2026-12-31 |
การทำงานกับข้อมูลทางคลินิก
สร้าง Observation (สัญญาณชีพ)
บันทึกสัญญาณชีพใหม่:
const createObservation = async (observationData) => {
const observation = {
resourceType: 'Observation',
status: 'final',
category: [
{
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/observation-category',
code: 'vital-signs'
}]
}
],
code: {
coding: [{
system: 'http://loinc.org',
code: observationData.code,
display: observationData.display
}]
},
subject: {
reference: `Patient/${observationData.patientId}`
},
effectiveDateTime: observationData.effectiveDate || new Date().toISOString(),
valueQuantity: {
value: observationData.value,
unit: observationData.unit,
system: 'http://unitsofmeasure.org',
code: observationData.ucumCode
},
performer: [
{
reference: `Practitioner/${observationData.performerId}`
}
]
};
const response = await fhirRequest('/Observation', {
method: 'POST',
body: JSON.stringify(observation)
});
return response;
};
// การใช้งาน
const systolicBP = await createObservation({
patientId: '12345',
code: '8480-6',
display: 'ความดันโลหิตซิสโตลิก',
value: 120,
unit: 'mmHg',
ucumCode: 'mm[Hg]',
performerId: '67890'
});
console.log(`Observation สร้างแล้ว: ${systolicBP.id}`);
รหัส LOINC ที่ใช้บ่อย
| รหัส (Code) | การแสดงผล (Display) | หมวดหมู่ (Category) |
|---|---|---|
| 8480-6 | ความดันโลหิตซิสโตลิก | สัญญาณชีพ |
| 8462-4 | ความดันโลหิตไดแอสโตลิก | สัญญาณชีพ |
| 8867-4 | อัตราการเต้นของหัวใจ | สัญญาณชีพ |
| 8310-5 | อุณหภูมิร่างกาย | สัญญาณชีพ |
| 8302-2 | ส่วนสูงของร่างกาย | สัญญาณชีพ |
| 29463-7 | น้ำหนักตัว | สัญญาณชีพ |
| 8871-5 | อัตราการหายใจ | สัญญาณชีพ |
| 2339-0 | กลูโคส [มวล/ปริมาตร] | ห้องปฏิบัติการ |
| 4548-4 | ฮีโมโกลบิน A1c | ห้องปฏิบัติการ |
| 2093-3 | คอเลสเตอรอล [มวล/ปริมาตร] | ห้องปฏิบัติการ |
การสร้าง Condition (รายการปัญหา)
เพิ่มการวินิจฉัยใหม่:
const createCondition = async (conditionData) => {
const condition = {
resourceType: 'Condition',
clinicalStatus: {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
code: conditionData.status || 'active'
}]
},
verificationStatus: {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/condition-ver-status',
code: 'confirmed'
}]
},
category: [
{
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/condition-category',
code: conditionData.category || 'problem-list-item'
}]
}
],
code: {
coding: [{
system: 'http://snomed.info/sct',
code: conditionData.sctCode,
display: conditionData.display
}]
},
subject: {
reference: `Patient/${conditionData.patientId}`
},
onsetDateTime: conditionData.onsetDate,
recordedDate: new Date().toISOString()
};
const response = await fhirRequest('/Condition', {
method: 'POST',
body: JSON.stringify(condition)
});
return response;
};
// การใช้งาน
const diabetes = await createCondition({
patientId: '12345',
sctCode: '44054006',
display: 'โรคเบาหวานชนิดที่ 2',
status: 'active',
category: 'problem-list-item',
onsetDate: '2024-01-15'
});
รหัส SNOMED CT ที่ใช้บ่อย
| รหัส (Code) | การแสดงผล (Display) | หมวดหมู่ (Category) |
|---|---|---|
| 44054006 | โรคเบาหวานชนิดที่ 2 | ปัญหา |
| 38341003 | ความดันโลหิตสูง | ปัญหา |
| 195967001 | โรคหอบหืด | ปัญหา |
| 13645005 | โรคปอดอุดกั้นเรื้อรัง | ปัญหา |
| 35489007 | โรคซึมเศร้า | ปัญหา |
| 22298006 | กล้ามเนื้อหัวใจตาย | ปัญหา |
| 26929004 | โรคอัลไซเมอร์ | ปัญหา |
| 396275006 | โรคข้อเข่าเสื่อม | ปัญหา |
การดึงข้อมูลยาของผู้ป่วย
ดึงรายการ MedicationRequest:
const getPatientMedications = async (patientId) => {
const response = await fhirRequest(
`/MedicationRequest?patient=${patientId}&status=active`
);
return response;
};
// การใช้งาน
const medications = await getPatientMedications('12345');
medications.entry?.forEach(entry => {
const med = entry.resource;
console.log(`${med.medicationCodeableConcept.coding[0].display}`);
console.log(` ขนาดยา: ${med.dosageInstruction[0]?.text}`);
console.log(` สถานะ: ${med.status}`);
});
การดึงผลตรวจทางห้องปฏิบัติการ
ดึง DiagnosticReport/Observation:
const getPatientLabResults = async (patientId, options = {}) => {
const params = new URLSearchParams({
patient: patientId,
category: options.category || 'laboratory'
});
if (options.dateFrom) {
params.append('date', `ge${options.dateFrom}`);
}
const response = await fhirRequest(`/DiagnosticReport?${params.toString()}`);
return response;
};
const getLabValue = async (patientId, loincCode) => {
const params = new URLSearchParams({
patient: patientId,
code: loincCode
});
const response = await fhirRequest(`/Observation?${params.toString()}`);
return response;
};
// การใช้งาน
const hba1c = await getLabValue('12345', '4548-4');
hba1c.entry?.forEach(entry => {
const obs = entry.resource;
console.log(`HbA1c: ${obs.valueQuantity.value} ${obs.valueQuantity.unit}`);
console.log(`วันที่: ${obs.effectiveDateTime}`);
});
OAuth 2.0 และ SMART on FHIR
ทำความเข้าใจการยืนยันตัวตน FHIR
เซิร์ฟเวอร์ FHIR ส่วนใหญ่ใช้ OAuth 2.0 + OpenID Connect:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ไคลเอนต์ │───▶│ เซิร์ฟเวอร์ │───▶│ เซิร์ฟเวอร์ │
│ (แอป) │ │ ตรวจสอบสิทธิ์ │ │ FHIR │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ 1. คำขอตรวจสอบสิทธิ์ │ │
│───────────────────▶│ │
│ │ │
│ 2. ผู้ใช้เข้าสู่ระบบ │ │
│◀───────────────────│ │
│ │ │
│ 3. รหัสตรวจสอบสิทธิ์ │ │
│───────────────────▶│ │
│ │ │
│ 4. คำขอโทเค็น │ │
│───────────────────▶│ │
│ │ 5. โทเค็น + ID │
│◀───────────────────│ │
│ │ │
│ 6. คำขอ API │ │
│────────────────────────────────────────▶│
│ │ │
│ 7. ข้อมูล FHIR │ │
│◀────────────────────────────────────────│
การเปิดแอป SMART on FHIR
ตัวอย่างการ flow SMART on FHIR:
const crypto = require('crypto');
class SMARTClient {
constructor(config) {
this.clientId = config.clientId;
this.redirectUri = config.redirectUri;
this.issuer = config.issuer;
this.scopes = config.scopes;
}
generatePKCE() {
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
return { codeVerifier, codeChallenge };
}
buildAuthUrl(state, patientId = null) {
const { codeVerifier, codeChallenge } = this.generatePKCE();
this.codeVerifier = codeVerifier;
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: this.scopes.join(' '),
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
aud: this.issuer,
launch: patientId ? `patient-${patientId}` : null
});
return `${this.issuer}/authorize?${params.toString()}`;
}
async exchangeCodeForToken(code) {
const response = await fetch(`${this.issuer}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: this.redirectUri,
client_id: this.clientId,
code_verifier: this.codeVerifier
})
});
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
patientId: data.patient,
encounterId: data.encounter
};
}
}
// การใช้งาน
const smartClient = new SMARTClient({
clientId: 'my-app-client-id',
redirectUri: 'https://myapp.com/callback',
issuer: 'https://fhir.epic.com',
scopes: [
'openid',
'profile',
'patient/Patient.read',
'patient/Observation.read',
'patient/Condition.read',
'patient/MedicationRequest.read',
'offline_access'
]
});
// เปลี่ยนเส้นทางผู้ใช้ไปยัง URL การตรวจสอบสิทธิ์
const state = crypto.randomBytes(16).toString('hex');
const authUrl = smartClient.buildAuthUrl(state);
console.log(`เปลี่ยนเส้นทางไปที่: ${authUrl}`);
ขอบเขต SMART ที่จำเป็น
| ขอบเขต (Scope) | สิทธิ์ (Permission) | กรณีการใช้งาน (Use Case) |
|---|---|---|
openid |
การตรวจสอบสิทธิ์ OIDC | จำเป็นสำหรับทุกแอป |
profile |
ข้อมูลโปรไฟล์ผู้ใช้ | ไดเรกทอรีผู้ให้บริการ |
patient/Patient.read |
อ่านข้อมูลประชากรของผู้ป่วย | การค้นหาผู้ป่วย |
patient/Observation.read |
อ่านข้อมูลสังเกตการณ์ | สัญญาณชีพ, ผลตรวจทางห้องปฏิบัติการ |
patient/Condition.read |
อ่าน Condition | รายการปัญหา |
patient/MedicationRequest.read |
อ่านยา | ประวัติการใช้ยา |
patient/*.read |
อ่านทรัพยากรผู้ป่วยทั้งหมด | ข้อมูลผู้ป่วยทั้งหมด |
user/*.read |
อ่านทรัพยากรทั้งหมดที่เข้าถึงได้ | มุมมองผู้ให้บริการ |
offline_access |
รีเฟรชโทเค็น | เซสชันที่ใช้งานได้นาน |
ส่งคำขอ FHIR ที่ผ่านการยืนยันตัวตนแล้ว
class FHIRClient {
constructor(accessToken, fhirBaseUrl) {
this.accessToken = accessToken;
this.baseUrl = fhirBaseUrl;
}
async request(endpoint, options = {}) {
const response = await fetch(`${this.baseUrl}/fhir${endpoint}`, {
...options,
headers: {
'Accept': 'application/fhir+json',
'Authorization': `Bearer ${this.accessToken}`,
...options.headers
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(`FHIR Error: ${error.issue?.[0]?.details?.text}`);
}
return response.json();
}
async getPatient(patientId) {
return this.request(`/Patient/${patientId}`);
}
async searchPatients(params) {
const query = new URLSearchParams(params);
return this.request(`/Patient?${query.toString()}`);
}
}
// หลัง OAuth callback
const fhirClient = new FHIRClient(tokens.accessToken, 'https://fhir.epic.com');
const patient = await fhirClient.getPatient(tokens.patientId);
การดำเนินการแบบ Batch และ Transaction
คำขอแบบ Batch
ส่งหลาย request อิสระในรอบเดียว:
const batchRequest = async (requests) => {
const bundle = {
resourceType: 'Bundle',
type: 'batch',
entry: requests.map(req => ({
resource: req.resource,
request: {
method: req.method,
url: req.url
}
}))
};
const response = await fhirRequest('', {
method: 'POST',
body: JSON.stringify(bundle)
});
return response;
};
// ตัวอย่าง
const bundle = await batchRequest([
{ method: 'GET', url: 'Patient/12345' },
{ method: 'GET', url: 'Patient/12345/Observation?category=vital-signs' },
{ method: 'GET', url: 'Patient/12345/Condition?clinical-status=active' }
]);
bundle.entry.forEach((entry, index) => {
console.log(`การตอบกลับ ${index}: ${entry.response.status}`);
console.log(entry.resource);
});
คำขอแบบ Transaction
atomic operation หลายรายการ:
const transactionRequest = async (requests) => {
const bundle = {
resourceType: 'Bundle',
type: 'transaction',
entry: requests.map(req => ({
resource: req.resource,
request: {
method: req.method,
url: req.url
}
}))
};
const response = await fhirRequest('', {
method: 'POST',
body: JSON.stringify(bundle)
});
return response;
};
// ตัวอย่าง
const transaction = await transactionRequest([
{
method: 'POST',
url: 'Patient',
resource: {
resourceType: 'Patient',
name: [{ family: 'Doe', given: ['Jane'] }],
gender: 'female',
birthDate: '1990-01-01'
}
},
{
method: 'POST',
url: 'Condition',
resource: {
resourceType: 'Condition',
clinicalStatus: { coding: [{ code: 'active' }] },
code: { coding: [{ system: 'http://snomed.info/sct', code: '38341003' }] },
subject: { reference: 'Patient/-1' }
}
}
]);
การสมัครสมาชิกและ Webhook
การสมัครสมาชิก FHIR (R4B+)
รับแจ้งเตือนการเปลี่ยนแปลงข้อมูล:
const createSubscription = async (subscriptionData) => {
const subscription = {
resourceType: 'Subscription',
status: 'requested',
criteria: subscriptionData.criteria,
reason: subscriptionData.reason,
channel: {
type: 'rest-hook',
endpoint: subscriptionData.endpoint,
payload: 'application/fhir+json'
}
};
const response = await fhirRequest('/Subscription', {
method: 'POST',
body: JSON.stringify(subscription)
});
return response;
};
// สมัครสมาชิก
const subscription = await createSubscription({
criteria: 'DiagnosticReport?category=laboratory&patient=12345',
reason: 'ติดตามผลการตรวจทางห้องปฏิบัติการของผู้ป่วย',
endpoint: 'https://myapp.com/webhooks/fhir'
});
การจัดการ FHIR Webhook
const express = require('express');
const app = express();
app.post('/webhooks/fhir', express.json({ type: 'application/fhir+json' }), async (req, res) => {
const notification = req.body;
// ตรวจสอบการอ้างอิงการสมัครสมาชิก
if (notification.subscription !== expectedSubscription) {
return res.status(401).send('ไม่ได้รับอนุญาต');
}
// ประมวลผลการแจ้งเตือน
if (notification.event?.resourceType === 'DiagnosticReport') {
const reportId = notification.event.resourceId;
const report = await fhirRequest(`/DiagnosticReport/${reportId}`);
// ประมวลผลผลการตรวจทางห้องปฏิบัติการใหม่
await processLabResult(report);
}
res.status(200).send('ตกลง');
});
การแก้ไขปัญหาทั่วไป
401 Unauthorized
อาการ: Unauthorized/Invalid token
แนวทางแก้ไข:
- ตรวจสอบอายุ token
- ตรวจสอบ scope ครอบคลุม resource
- ตรวจ header
Authorization: Bearer {token} - ตรวจ audience ของ token ตรงกับ FHIR server
403 Forbidden
อาการ: token ถูกต้อง แต่โดนปฏิเสธ
แนวทางแก้ไข:
- ตรวจสิทธิ์ผู้ใช้
- ตรวจ context ผู้ป่วย
- ตรวจ SMART scopes
- ตรวจ resource-level access
404 Not Found
อาการ: ไม่พบ resource/endpoint
แนวทางแก้ไข:
- ตรวจ ID resource
- ตรวจ base URL
- ตรวจว่า server รองรับ resource
- ตรวจ endpoint เวอร์ชัน
422 Unprocessable Entity
อาการ: validation error ตอน create/update
แนวทางแก้ไข:
// แยกวิเคราะห์ข้อผิดพลาดในการตรวจสอบความถูกต้อง
const error = await response.json();
error.issue?.forEach(issue => {
console.log(`ความรุนแรง: ${issue.severity}`);
console.log(`ตำแหน่ง: ${issue.expression?.join('.')}`);
console.log(`ข้อความ: ${issue.details?.text}`);
});
สาเหตุทั่วไป
- ฟิลด์ที่จำเป็นขาด
- รหัสไม่ถูกต้อง
- อ้างอิงผิดรูปแบบ
- ปัญหารูปแบบวันที่
รายการตรวจสอบก่อน deploy production
- [ ] ตั้งค่า OAuth 2.0 + SMART on FHIR
- [ ] รองรับ refresh token
- [ ] จัดการ error ครบถ้วน
- [ ] Logging ไม่มี PHI
- [ ] Rate limiting
- [ ] Retry + exponential backoff
- [ ] ทดสอบกับ EHR หลายเจ้า
- [ ] ตรวจสอบกับ FHIR validator
- [ ] ทำเอกสาร FHIR API
- [ ] Monitoring/Alerting
- [ ] Runbook สำหรับ incident
การตรวจสอบความถูกต้องของ FHIR
const { FhirValidator } = require('fhir-validator');
const validator = new FhirValidator('4.0.1');
const validateResource = async (resource) => {
const validationResult = await validator.validate(resource);
if (!validationResult.valid) {
validationResult.issues.forEach(issue => {
console.error(`ข้อผิดพลาดในการตรวจสอบความถูกต้อง: ${issue.message}`);
console.error(`ตำแหน่ง: ${issue.path}`);
});
throw new Error('การตรวจสอบความถูกต้องของทรัพยากรล้มเหลว');
}
return true;
};
// ใช้ก่อนสร้าง/อัปเดต
await validateResource(patientResource);
กรณีการใช้งานจริง
การบูรณาการ Patient Portal
- ปัญหา: ผู้ป่วยเข้าถึงเวชระเบียนข้ามผู้ให้บริการไม่ได้
- แนวทาง: แอป SMART on FHIR เชื่อมต่อ Epic & Cerner
- ผลลัพธ์: ผู้ป่วยใช้งาน 80%, ลดคำขอเวชระเบียน 50%
- Implementation: SMART app ฝั่งคนไข้, read-only Patient/Observation/Condition/MedicationRequest, refresh token, responsive UI
การสนับสนุนการตัดสินใจทางคลินิก (CDS)
- ปัญหา: provider มองข้ามโอกาสป้องกันโรค
- แนวทาง: query FHIR ช่องว่างการดูแลแบบ real-time
- ผลลัพธ์: HEDIS score ดีขึ้น 25%
- Implementation: SMART app ฝั่ง provider, สอบถาม Patient/Condition/Observation/Immunization, ประมวลผล gap-in-care บน workflow EHR
การวิเคราะห์สุขภาพประชากร
- ปัญหา: ข้อมูลไม่สมบูรณ์ใน network
- แนวทาง: Bulk FHIR export สำหรับ data analytics
- ผลลัพธ์: 360° patient view, ลดต้นทุน PMPM
- Implementation: bulk FHIR ($export), ส่งออก nightly, risk stratification, alert สำหรับ care manager
บทสรุป
HL7 FHIR เป็นพื้นฐาน interoperability สุขภาพยุคใหม่ ข้อควรเน้น:
- FHIR R4 คือมาตรฐาน API สุขภาพปัจจุบัน
- SMART on FHIR เพิ่ม OAuth 2.0 ที่ปลอดภัย
- Resource type มาตรฐานข้อมูลผู้ป่วย, observation, condition, medication
- Search parameters ทำ query ข้อมูลยืดหยุ่น
- Batch/transaction รองรับ workflow ซับซ้อน
- Apidog ช่วยทดสอบและทำเอกสาร FHIR API
ส่วนคำถามที่พบบ่อย
HL7 FHIR ใช้สำหรับอะไร?
FHIR ทำให้การแลกเปลี่ยนข้อมูลสุขภาพเป็นมาตรฐานระหว่าง EHR, patient portal, mobile app และระบบสุขภาพอื่นๆ ใช้กับแอปผู้ป่วย, CDS, population health, care coordination
ฉันจะเริ่มต้นใช้งาน FHIR ได้อย่างไร?
เริ่มจาก public FHIR server (HAPI FHIR test) หรือ FHIR cloud service (Azure, AWS) ฝึกอ่าน resource และค้นหาด้วย parameter
HL7 v2 กับ FHIR แตกต่างกันอย่างไร?
HL7 v2 ใช้ข้อความ pipe-delimited (ADT, ORM, ORU) แบบ event-driven
FHIR ใช้ REST API + JSON/XML แบบ resource-based เหมาะกับเว็บ/มือถือ
FHIR ปฏิบัติตาม HIPAA หรือไม่?
FHIR เป็น data standard ส่วน compliance ขึ้นกับการใช้งานจริง: encryption, auth, access control, audit ใช้ OAuth 2.0 กับ SMART on FHIR เพื่อความปลอดภัย
SMART scopes คืออะไร?
SMART scopes กำหนดสิทธิ์ละเอียด เช่น patient/Observation.read, user/*.read ควรขอเฉพาะ scope ที่จำเป็น
ฉันจะค้นหาทรัพยากรใน FHIR ได้อย่างไร?
ใช้ GET พารามิเตอร์: /Patient?name=Smith&birthdate=ge1980-01-01
รองรับ modifier (:exact, :contains) และ prefix (gt, lt, ge, le)
Bulk FHIR คืออะไร?
Bulk FHIR ($export) ส่งออกข้อมูลเป็น NDJSON ชุดใหญ่ ใช้กับ population health, analytics, data warehouse
ฉันจะจัดการเวอร์ชัน FHIR ได้อย่างไร?
ระบุเวอร์ชัน endpoint (แนะนำ R4) ตรวจสอบ CapabilityStatement ว่า server รองรับอะไร
ฉันสามารถขยาย FHIR ด้วยฟิลด์ที่กำหนดเองได้หรือไม่?
ได้ ใช้ FHIR extension เพิ่ม custom field กำหนดใน Implementation Guide และลงทะเบียนกับ HL7 ถ้าจะเผยแพร่
มีเครื่องมือใดบ้างที่ช่วยในการพัฒนา FHIR?
เครื่องมือยอดนิยม: HAPI FHIR (โอเพนซอร์ส), FHIR validator, Postman collection, Apidog สำหรับทดสอบและทำเอกสาร API

Top comments (0)