DEV Community

Cover image for I Connected a Patient Portal with an EHR System Using FHIR APIs
Kamal Deep Pareek
Kamal Deep Pareek

Posted on

I Connected a Patient Portal with an EHR System Using FHIR APIs

Healthcare software has one recurring bottleneck: getting different systems to actually talk to each other. Electronic Health Record (EHR) platforms like Epic, Cerner, and Allscripts store patient data in their own proprietary formats, and until FHIR (Fast Healthcare Interoperability Resources) became the industry standard, connecting a patient-facing app to that data meant custom point-to-point integrations for every vendor.

I recently went through the process of connecting a patient portal to an EHR system using FHIR APIs, and I wanted to document the actual steps, the decisions that mattered, and the mistakes that cost time. This is written for developers who are approaching patient portal development for the first time and want a realistic picture of what the integration looks like — not just the marketing version.

Why FHIR Instead of HL7 v2 or a Custom API

Before FHIR, most EHR integrations relied on HL7 v2 messaging, which uses a pipe-delimited format that's painful to parse and inconsistent across vendors. FHIR replaced that with RESTful resources over HTTP, using JSON or XML payloads that map cleanly onto how modern web and mobile apps are built.

For patient portal development specifically, FHIR gives you standardized resources for the things a portal actually needs to display:

• Patient — demographics and identifiers
• Observation — vitals, lab results
• Condition — diagnoses
• MedicationRequest — prescriptions
• AllergyIntolerance — allergy records
• Appointment — scheduling data
• DocumentReference — clinical notes and attachments

Each of these is a predictable JSON structure, which means the portal's frontend doesn't need custom parsing logic per EHR vendor — assuming the EHR's FHIR implementation is reasonably compliant.

Step 1: Registering the App with the EHR

Every major EHR vendor exposes a developer sandbox before granting production access. I registered the patient portal as an app in the EHR vendor's developer portal, which required:
• App name and redirect URI
• Requested FHIR scopes (read-only patient data, in this case)
• Confirmation of SMART on FHIR launch type (standalone vs. EHR-launch)
Since this was a patient-facing portal rather than a clinician-facing tool embedded inside the EHR, I used the standalone launch flow rather than EHR-launch.

Step 2: Authentication with SMART on FHIR

This is where most of the real engineering effort went. FHIR itself doesn't define authentication — that's handled by the SMART on FHIR specification, which layers OAuth2 on top of the FHIR REST API.
The flow looks like this:

  1. Redirect the patient to the EHR's authorization endpoint with the requested scopes (e.g., patient/Observation.read, patient/Condition.read)
  2. Patient logs in and consents to sharing their data with the portal
  3. EHR redirects back with an authorization code
  4. Portal backend exchanges that code for an access token and refresh token
  5. Access token is attached as a Bearer token on every subsequent FHIR API call A few details that aren't obvious until you hit them: • Access tokens are typically short-lived (often under an hour), so refresh token handling isn't optional — it has to be built in from day one. • Scopes are granular per resource type and per action (read vs. write), so the initial scope request has to be planned around exactly what the portal will display. Under-scoping means silent 403s later. • Some EHR sandboxes return a patient context parameter in the token response — this is the FHIR patient ID you'll use in every subsequent query, and it's easy to miss if you're only reading the access token field.

Step 3: Querying FHIR Resources

Once authenticated, pulling patient data is a straightforward REST call:
GET [base]/Observation?patient={patientId}&category=vital-signs
Authorization: Bearer {access_token}

Accept: application/fhir+json
The response comes back as a FHIR Bundle — a wrapper resource containing an array of matching entries. I built a thin normalization layer on the backend that flattened these bundles into simpler objects for the frontend, since raw FHIR resources carry a lot of metadata (meta, extension fields, coding systems) that the portal UI doesn't need to render directly.

Pagination matters here too — bundles include link entries with next URLs, and it's easy to build a first version that only reads the first page and silently drops older records.

Step 4: Handling Vendor Differences

FHIR is a standard, but implementations vary. Two EHR sandboxes claiming R4 compliance still differed in:
• Which optional fields they actually populated
• How they represented coded values (different terminology bindings for the same concept)
• Rate limits on the sandbox environment
• Whether DocumentReference attachments were returned inline or as separate binary fetches
None of this is a FHIR problem exactly — it's a "the spec allows flexibility and vendors used it" problem. The practical fix was writing a vendor-adapter layer rather than assuming one integration path would work unmodified across EHR systems.

Step 5: Mapping to the Portal UI

With normalized data flowing in, the last piece was mapping FHIR resources to portal screens: an appointments tab reading from Appointment, a medications list from MedicationRequest, a results view from Observation and DiagnosticReport. Keeping this mapping layer separate from the raw FHIR client made it much easier to swap or add a second EHR connection later without touching the UI code.

What I'd Tell Someone Starting This Today

If you're starting patient portal development with a FHIR integration, a few things are worth planning for before writing any code:

• Decide your launch type (standalone vs. EHR-launch) early — it changes the auth flow structure
• Scope your OAuth2 requests precisely to what you'll actually query
• Build refresh token handling from the start, not as an afterthought
• Treat "FHIR compliant" as a starting point, not a guarantee of identical behavior across vendors
• Keep a normalization layer between raw FHIR bundles and your UI components

FHIR made this integration dramatically more manageable than it would have been with older HL7 v2 messaging or a fully custom API. But it's still an integration project with real edge cases, not a plug-and-play connector — and treating it that way from the start saved a lot of rework later.

Top comments (0)