DEV Community

Cover image for How to Use HL7 FHIR API: Complete Healthcare Integration Guide (2026)
Preecha
Preecha

Posted on

How to Use HL7 FHIR API: Complete Healthcare Integration Guide (2026)

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.

Try Apidog today

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.

Image

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}
Enter fullscreen mode Exit fullscreen mode

For example:

GET /fhir/Patient/12345
GET /fhir/Observation?patient=12345
POST /fhir/Condition
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Store credentials outside your source code:

export FHIR_BASE_URL="https://your-fhir-server.example"
export FHIR_TOKEN="your-access-token"
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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" }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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]}`);
});
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

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'
});
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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      │                    │
     │◀────────────────────────────────────────│
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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'
      }
    }
  }
]);
Enter fullscreen mode Exit fullscreen mode

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'
});
Enter fullscreen mode Exit fullscreen mode

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');
  }
);
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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, and MedicationRequest.
  • 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, and Immunization.
  • 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, and MedicationRequest.
  • Use search parameters and prefixes to build flexible queries.
  • Use batch and transaction Bundles for multi-resource workflows.
  • Validate resources and handle OperationOutcome errors 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
Enter fullscreen mode Exit fullscreen mode

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)