TL;DR
HL7 FHIR (Fast Healthcare Interoperability Resources) is a modern standard for healthcare data exchange. It uses RESTful APIs with JSON or XML payloads and defines standardized resources for patients, observations, medications, and more. This guide shows how to discover FHIR capabilities, read and search resources, authenticate with OAuth 2.0 and SMART on FHIR, handle batch operations, and prepare an integration for production.
Introduction
Healthcare data fragmentation costs the U.S. healthcare system $30 billion annually. For developers building healthcare applications, HL7 FHIR API integration is an industry standard mandated by CMS and adopted by Epic, Cerner, and other major EHR vendors.
FHIR-enabled applications can reduce care-coordination time and replace fax-based record requests with API-driven workflows. A FHIR integration lets you exchange data across EHRs, patient portals, and care-coordination platforms using a consistent resource model.
This guide focuses on implementation: FHIR architecture, resource types, search parameters, OAuth 2.0 authentication, SMART on FHIR integration, and production deployment practices.
💡 Apidog can help you test FHIR endpoints, validate resource schemas, debug authentication flows, import FHIR Implementation Guides, mock responses, and share test scenarios with your team.
What Is HL7 FHIR?
FHIR (Fast Healthcare Interoperability Resources) is a standards framework for exchanging healthcare information electronically. Developed by Health Level Seven International (HL7), FHIR uses modern web technologies including RESTful APIs, JSON, XML, and OAuth 2.0.
FHIR Resource Types
FHIR defines 140+ resource types. These are the resources you will use most often in application integrations:
| Resource | Purpose | Common use cases |
|---|---|---|
Patient |
Demographics | Patient lookup, registration |
Practitioner |
Provider information | Directories, scheduling |
Encounter |
Visits and admissions | Care episodes, billing |
Observation |
Clinical data | Vitals, lab results, assessments |
Condition |
Problems and diagnoses | Problem lists, care planning |
MedicationRequest |
Prescriptions | E-prescribing, medication history |
AllergyIntolerance |
Allergies | Safety checks, alerts |
Immunization |
Vaccinations | Immunization records |
DiagnosticReport |
Lab and imaging reports | Results delivery |
DocumentReference |
Clinical documents | CCDs, discharge summaries |
FHIR API Architecture
FHIR resources are typically exposed through RESTful endpoints:
https://fhir-server.com/fhir/{resourceType}/{id}
For example:
GET /fhir/Patient/12345
GET /fhir/Observation?patient=12345
POST /fhir/Condition
Before calling a server, request its CapabilityStatement from the /metadata endpoint. This tells you which FHIR version, resources, search parameters, and operations the server supports.
FHIR Versions Compared
| Version | Status | Use case |
|---|---|---|
| R4 (4.0.1) | Current STU | Production implementations |
| R4B (4.3) | Trial Implementation | Early adopters |
| R5 (5.0.0) | Draft STU | Future implementations |
| DSTU2 | Deprecated | Legacy systems only |
CMS requires Certified EHRs to support FHIR R4 for Patient Access and Provider Access APIs. For new integrations, target R4 unless the server documentation explicitly requires another version.
Getting Started: FHIR Server Access
Step 1: Choose Your FHIR Server
Choose a server based on your cloud environment, EHR vendor, and deployment requirements.
| Server | Type | Cost | Best for |
|---|---|---|---|
| Azure API for FHIR | Managed | Pay-per-use | Enterprise and Azure environments |
| AWS HealthLake | Managed | Pay-per-use | AWS environments |
| Google Cloud Healthcare API | Managed | Pay-per-use | GCP environments |
| HAPI FHIR | Open source | Self-hosted | Custom deployments |
| Epic FHIR Server | Commercial | Epic customers | Epic EHR integration |
| Cerner Ignite FHIR | Commercial | Cerner customers | Cerner EHR integration |
Step 2: Get Server Credentials
Managed FHIR servers typically require an endpoint and an OAuth 2.0 client registration.
# Azure API for FHIR
# 1. Create a FHIR Service in Azure Portal.
# 2. Configure authentication with OAuth 2.0 or Azure AD.
# 3. Get the FHIR endpoint:
# https://{service-name}.azurehealthcareapis.com
# 4. Register a client application for OAuth.
# AWS HealthLake
# 1. Create a data store in AWS Console.
# 2. Configure IAM roles.
# 3. Get the endpoint:
# https://healthlake.{region}.amazonaws.com
Store credentials outside your source code:
export FHIR_BASE_URL="https://your-fhir-server.example"
export FHIR_TOKEN="your-access-token"
Step 3: Understand FHIR REST Operations
| Operation | HTTP method | Endpoint | Description |
|---|---|---|---|
| Read | GET |
/{resourceType}/{id} |
Get a specific resource |
| Search | GET |
/{resourceType}?param=value |
Search resources |
| Create | POST |
/{resourceType} |
Create a resource |
| Update | PUT |
/{resourceType}/{id} |
Replace a resource |
| Patch | PATCH |
/{resourceType}/{id} |
Partially update a resource |
| Delete | DELETE |
/{resourceType}/{id} |
Remove a resource |
| History | GET |
/{resourceType}/{id}/_history |
Get resource versions |
Step 4: Make Your First FHIR Call
Call /metadata to verify connectivity and inspect server capabilities:
curl -X GET "https://fhir-server.com/fhir/metadata" \
-H "Accept: application/fhir+json" \
-H "Authorization: Bearer {token}"
A server may return a response like this:
{
"resourceType": "CapabilityStatement",
"status": "active",
"date": "2026-03-25",
"fhirVersion": "4.0.1",
"rest": [
{
"mode": "server",
"resource": [
{ "type": "Patient" },
{ "type": "Observation" },
{ "type": "Condition" }
]
}
]
}
Use this response to confirm:
- The supported FHIR version.
- Available resource types.
- Supported search parameters.
- Available operations such as
$export. - Security requirements.
Core FHIR Operations
Reading a Patient Resource
Create a reusable request helper before implementing individual resources:
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();
};
const getPatient = async (patientId) => {
return fhirRequest(`/Patient/${patientId}`);
};
// Usage
const patient = await getPatient('12345');
console.log(`Patient: ${patient.name[0].given[0]} ${patient.name[0].family}`);
console.log(`DOB: ${patient.birthDate}`);
console.log(`Gender: ${patient.gender}`);
Patient Resource Structure
A Patient resource stores demographic information and identifiers:
{
"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"
}
]
}
Searching for Resources
FHIR search uses query parameters. Build them with URLSearchParams so values are correctly encoded.
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);
}
return fhirRequest(`/Patient?${query.toString()}`);
};
// Usage
const results = await searchPatients({
name: 'Smith',
birthDate: '1985-06-15'
});
console.log(`Found ${results.total} patients`);
results.entry?.forEach(({ resource: patient }) => {
console.log(`${patient.name[0].family}, ${patient.name[0].given[0]}`);
});
Search results are returned as a FHIR Bundle. Always use optional chaining for entry, because an empty result can omit it.
Common Search Parameters
| 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 |
Check the server CapabilityStatement before relying on a parameter. FHIR defines common parameters, but individual servers can vary in what they support.
Search Modifiers
| Modifier | Description | Example |
|---|---|---|
:exact |
Exact match | name:exact=Smith |
:contains |
Contains text | name:contains=smi |
:missing |
Has or lacks a value | phone:missing=true |
| Prefix operators | Compare values and dates | birthdate=ge1980-01-01 |
Search Prefixes for Dates and Numbers
| Prefix | Meaning | Example |
|---|---|---|
eq |
Equal to | birthdate=eq1985-06-15 |
ne |
Not equal to | birthdate=ne1985-06-15 |
gt |
Greater than | birthdate=gt1980-01-01 |
lt |
Less than | birthdate=lt1990-01-01 |
ge |
Greater than or equal | birthdate=ge1980-01-01 |
le |
Less than or equal | birthdate=le1990-01-01 |
sa |
Starts after | date=sa2026-01-01 |
eb |
Ends before | date=eb2026-12-31 |
Working with Clinical Data
Creating an Observation for Vital Signs
Use Observation for vital signs, lab values, and other measured clinical data. Use LOINC codes for the observation type and UCUM codes for units.
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}`
}
]
};
return fhirRequest('/Observation', {
method: 'POST',
headers: {
'Content-Type': 'application/fhir+json'
},
body: JSON.stringify(observation)
});
};
// Usage: record systolic blood pressure
const systolicBP = await createObservation({
patientId: '12345',
code: '8480-6',
display: 'Systolic blood pressure',
value: 120,
unit: 'mmHg',
ucumCode: 'mm[Hg]',
performerId: '67890'
});
console.log(`Observation created: ${systolicBP.id}`);
Common LOINC Codes
| Code | Display | Category |
|---|---|---|
8480-6 |
Systolic blood pressure | Vital signs |
8462-4 |
Diastolic blood pressure | Vital signs |
8867-4 |
Heart rate | Vital signs |
8310-5 |
Body temperature | Vital signs |
8302-2 |
Body height | Vital signs |
29463-7 |
Body weight | Vital signs |
8871-5 |
Respiratory rate | Vital signs |
2339-0 |
Glucose [Mass/volume] | Laboratory |
4548-4 |
Hemoglobin A1c | Laboratory |
2093-3 |
Cholesterol [Mass/volume] | Laboratory |
Creating a Condition for a Problem List
Use Condition to represent diagnoses and problem-list entries. Use a terminology system such as SNOMED CT for the condition code.
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()
};
return fhirRequest('/Condition', {
method: 'POST',
headers: {
'Content-Type': 'application/fhir+json'
},
body: JSON.stringify(condition)
});
};
// Usage: add diabetes to a problem list
const diabetes = await createCondition({
patientId: '12345',
sctCode: '44054006',
display: 'Type 2 Diabetes Mellitus',
status: 'active',
category: 'problem-list-item',
onsetDate: '2024-01-15'
});
Common SNOMED CT Codes
| Code | Display | Category |
|---|---|---|
44054006 |
Type 2 Diabetes Mellitus | Problem |
38341003 |
Hypertension | Problem |
195967001 |
Asthma | Problem |
13645005 |
Chronic Obstructive Pulmonary Disease | Problem |
35489007 |
Depressive Disorder | Problem |
22298006 |
Myocardial Infarction | Problem |
26929004 |
Alzheimer’s Disease | Problem |
396275006 |
Osteoarthritis | Problem |
Retrieving Patient Medications
Query active MedicationRequest resources for a patient:
const getPatientMedications = async (patientId) => {
return fhirRequest(
`/MedicationRequest?patient=${patientId}&status=active`
);
};
// Usage
const medications = await getPatientMedications('12345');
medications.entry?.forEach(({ resource: med }) => {
console.log(med.medicationCodeableConcept.coding[0].display);
console.log(` Dose: ${med.dosageInstruction[0]?.text}`);
console.log(` Status: ${med.status}`);
});
Retrieving Lab Results
Use DiagnosticReport for report-level results and Observation for individual test values.
const getPatientLabResults = async (patientId, options = {}) => {
const params = new URLSearchParams({
patient: patientId,
category: options.category || 'laboratory'
});
if (options.dateFrom) {
params.append('date', `ge${options.dateFrom}`);
}
return fhirRequest(`/DiagnosticReport?${params.toString()}`);
};
const getLabValue = async (patientId, loincCode) => {
const params = new URLSearchParams({
patient: patientId,
code: loincCode
});
return fhirRequest(`/Observation?${params.toString()}`);
};
// Usage: get HbA1c results
const hba1c = await getLabValue('12345', '4548-4');
hba1c.entry?.forEach(({ resource: obs }) => {
console.log(`HbA1c: ${obs.valueQuantity.value} ${obs.valueQuantity.unit}`);
console.log(`Date: ${obs.effectiveDateTime}`);
});
OAuth 2.0 and SMART on FHIR
Understanding FHIR Authentication
FHIR servers commonly use OAuth 2.0 with OpenID Connect. SMART on FHIR standardizes how healthcare applications discover authorization endpoints, request scopes, launch within an EHR, and receive patient context.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client │───▶│ Auth │───▶│ FHIR │
│ (App) │ │ Server │ │ Server │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ 1. Auth Request │ │
│───────────────────▶│ │
│ │ │
│ 2. User Login │ │
│◀───────────────────│ │
│ │ │
│ 3. Auth Code │ │
│───────────────────▶│ │
│ │ │
│ 4. Token Request │ │
│───────────────────▶│ │
│ │ 5. Token + ID │
│◀───────────────────│ │
│ │ │
│ 6. API Request │ │
│────────────────────────────────────────▶│
│ │ │
│ 7. FHIR Data │ │
│◀────────────────────────────────────────│
SMART on FHIR App Launch
For authorization-code flows, use PKCE. Persist the generated state and code_verifier securely per user session so you can validate the callback and exchange the authorization code.
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();
// Store this per user session for the callback token exchange.
this.codeVerifier = codeVerifier;
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: this.scopes.join(' '),
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
aud: this.issuer
});
if (patientId) {
params.append('launch', `patient-${patientId}`);
}
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,
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'
]
});
const state = crypto.randomBytes(16).toString('hex');
const authUrl = smartClient.buildAuthUrl(state);
console.log(`Redirect to: ${authUrl}`);
Required SMART Scopes
Request only the scopes required by your application.
| Scope | Permission | Use case |
|---|---|---|
openid |
OIDC authentication | Required for all apps |
profile |
User profile information | Provider directory |
patient/Patient.read |
Read patient demographics | Patient lookup |
patient/Observation.read |
Read observations | Vitals and labs |
patient/Condition.read |
Read conditions | Problem lists |
patient/MedicationRequest.read |
Read medications | Medication history |
patient/*.read |
Read all patient resources | Full patient data |
user/*.read |
Read all accessible resources | Provider view |
offline_access |
Refresh token | Long-lived sessions |
Making Authenticated FHIR Requests
After the OAuth callback, use the access token for FHIR API requests:
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()}`);
}
}
// Usage after OAuth callback
const fhirClient = new FHIRClient(
tokens.accessToken,
'https://fhir.epic.com'
);
const patient = await fhirClient.getPatient(tokens.patientId);
Batch and Transaction Operations
Batch Requests
A batch Bundle executes multiple independent requests. Individual requests can succeed or fail independently.
const batchRequest = async (requests) => {
const bundle = {
resourceType: 'Bundle',
type: 'batch',
entry: requests.map((req) => ({
resource: req.resource,
request: {
method: req.method,
url: req.url
}
}))
};
return fhirRequest('', {
method: 'POST',
headers: {
'Content-Type': 'application/fhir+json'
},
body: JSON.stringify(bundle)
});
};
// Usage: fetch multiple resource types
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(`Response ${index}: ${entry.response.status}`);
console.log(entry.resource);
});
Transaction Requests
A transaction Bundle executes operations as an atomic unit. Use it when related writes must either all succeed or all fail.
const transactionRequest = async (requests) => {
const bundle = {
resourceType: 'Bundle',
type: 'transaction',
entry: requests.map((req) => ({
resource: req.resource,
request: {
method: req.method,
url: req.url
}
}))
};
return fhirRequest('', {
method: 'POST',
headers: {
'Content-Type': 'application/fhir+json'
},
body: JSON.stringify(bundle)
});
};
// Usage: create a patient and related resources
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'
}
}
}
]);
Subscriptions and Webhooks
FHIR Subscriptions (R4B+)
FHIR Subscriptions let your application receive notifications when resources change.
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'
}
};
return fhirRequest('/Subscription', {
method: 'POST',
headers: {
'Content-Type': 'application/fhir+json'
},
body: JSON.stringify(subscription)
});
};
// Usage: subscribe to new lab reports
const subscription = await createSubscription({
criteria: 'DiagnosticReport?category=laboratory&patient=12345',
reason: 'Monitor patient lab results',
endpoint: 'https://myapp.com/webhooks/fhir'
});
Handling FHIR Webhooks
Validate subscription information before processing notifications.
const express = require('express');
const app = express();
app.post(
'/webhooks/fhir',
express.json({ type: 'application/fhir+json' }),
async (req, res) => {
const notification = req.body;
// Verify the subscription reference.
if (notification.subscription !== expectedSubscription) {
return res.status(401).send('Unauthorized');
}
if (notification.event?.resourceType === 'DiagnosticReport') {
const reportId = notification.event.resourceId;
const report = await fhirRequest(`/DiagnosticReport/${reportId}`);
await processLabResult(report);
}
return res.status(200).send('OK');
}
);
Troubleshooting Common Issues
Issue: 401 Unauthorized
Symptoms: You receive Unauthorized or Invalid token errors.
Check:
- The access token has not expired.
- The token contains the scope required for the resource.
- The
Authorization: Bearer {token}header is present. - The FHIR server URL matches the token audience.
Issue: 403 Forbidden
Symptoms: The token is valid, but the server denies access.
Check:
- The user has permission to access the requested resource.
- The patient context matches for patient-scoped tokens.
- Requested SMART scopes include the required operation.
- Resource-level access controls permit the request.
Issue: 404 Not Found
Symptoms: The resource does not exist, or the endpoint is incorrect.
Check:
- The resource ID is correct.
- The FHIR base URL is correct.
- The server supports the requested resource type.
- You are using the expected FHIR version and endpoint format.
Issue: 422 Unprocessable Entity
Symptoms: The server returns validation errors on create or update.
Parse the OperationOutcome response:
const error = await response.json();
error.issue?.forEach((issue) => {
console.log(`Severity: ${issue.severity}`);
console.log(`Location: ${issue.expression?.join('.')}`);
console.log(`Message: ${issue.details?.text}`);
});
Common causes include:
- Missing required fields.
- Invalid code-system values.
- Incorrect FHIR reference format.
- Invalid date formats.
Production Deployment Checklist
Before going live, verify the following:
- [ ] Configure OAuth 2.0 with SMART on FHIR.
- [ ] Implement token-refresh logic.
- [ ] Add structured error handling.
- [ ] Add comprehensive logging without PHI.
- [ ] Implement rate limiting.
- [ ] Configure retries with exponential backoff.
- [ ] Test with multiple EHR vendors.
- [ ] Validate resources against a FHIR validator.
- [ ] Document every FHIR operation.
- [ ] Set up monitoring and alerting.
- [ ] Create a runbook for common issues.
FHIR Validation
Validate resources before sending create or update requests:
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(`Validation Error: ${issue.message}`);
console.error(`Location: ${issue.path}`);
});
throw new Error('Resource validation failed');
}
return true;
};
// Usage before create or update
await validateResource(patientResource);
Real-World Use Cases
Patient Portal Integration
A health system builds a patient portal.
- Challenge: Patients cannot access records from multiple providers.
- Solution: A SMART on FHIR app with Epic and Cerner integration.
- Result: 80% patient adoption and a 50% reduction in record requests.
Implementation approach:
- Build a patient-facing SMART app launch flow.
- Request read-only access to
Patient,Observation,Condition, andMedicationRequest. - Use refresh tokens for persistent sessions.
- Build a mobile-responsive user interface.
Clinical Decision Support
A care-management platform adds clinical decision support.
- Challenge: Providers miss preventive-care opportunities.
- Solution: Real-time FHIR queries for care gaps.
- Result: 25% improvement in HEDIS scores.
Implementation approach:
- Build a provider-facing SMART app.
- Query
Patient,Condition,Observation, andImmunization. - Calculate care gaps from guidelines.
- Show recommendations within the EHR workflow.
Population Health Analytics
A payer builds a population-health dashboard.
- Challenge: Data is incomplete across provider networks.
- Solution: FHIR Bulk Data export for analytics.
- Result: A 360-degree patient view and reduced PMPM costs.
Implementation approach:
- Use FHIR Bulk Data Access with
$export. - Run nightly exports to a data warehouse.
- Apply risk-stratification models.
- Generate care-manager alerts.
Conclusion
HL7 FHIR provides a foundation for modern healthcare interoperability.
Key implementation takeaways:
- Target FHIR R4 for healthcare API integrations.
- Use SMART on FHIR for OAuth 2.0 authorization.
- Model healthcare data with resources such as
Patient,Observation,Condition, andMedicationRequest. - Use search parameters and prefixes to build flexible queries.
- Use batch and transaction Bundles for multi-resource workflows.
- Validate resources and handle
OperationOutcomeerrors before production deployment. - Use Apidog to streamline FHIR API testing and documentation.
FAQ Section
What is HL7 FHIR used for?
FHIR enables standardized healthcare-data exchange between EHRs, patient portals, mobile apps, and other health IT systems. Common use cases include patient-access applications, clinical decision support, population health, and care coordination.
How do I get started with FHIR?
Start with a public FHIR server, such as the HAPI FHIR test server, or configure a cloud FHIR service such as Azure API for FHIR or AWS HealthLake. Begin by reading resources and testing search parameters.
What is the difference between HL7 v2 and FHIR?
HL7 v2 uses pipe-delimited messages such as ADT, ORM, and ORU for event-driven data exchange. FHIR uses RESTful APIs with JSON or XML for resource-based access. FHIR is better suited for modern web and mobile applications.
Is FHIR HIPAA-compliant?
FHIR is a data-format standard. HIPAA compliance depends on the implementation, including encryption, authentication, access controls, and audit logging. Use OAuth 2.0 with SMART on FHIR for secure access.
What are SMART scopes?
SMART scopes define granular permissions for FHIR resources, such as patient/Observation.read and user/*.read. Request only the scopes required by your application.
How do I search for resources in FHIR?
Use GET requests with query parameters:
/Patient?name=Smith&birthdate=ge1980-01-01
FHIR supports modifiers such as :exact and :contains, plus comparison prefixes such as gt, lt, ge, and le.
What is Bulk FHIR?
Bulk FHIR, through $export, enables asynchronous export of large datasets in NDJSON format. It is commonly used for population health, analytics, and data warehousing.
How do I handle FHIR versioning?
Target a specific FHIR version, typically R4, and use version-specific endpoints. Check the CapabilityStatement to identify supported versions, resources, operations, and search parameters.
Can I extend FHIR with custom fields?
Yes. Use FHIR extensions to add custom data elements. Define extensions in your Implementation Guide and register them with HL7 when sharing them broadly.
What tools help with FHIR development?
Common tools include HAPI FHIR, FHIR validators, Postman collections, and Apidog for API testing and documentation.

Top comments (0)