🏥

FHIR Integration

How J4H connects to clinical health records — the standard it uses, how the connection is made, the code that powers it, and why the strategy matters.

📋
What is FHIR?
The standard, the resources, and why it exists

FHIR stands for Fast Healthcare Interoperability Resources. It is a standard published by HL7 (Health Level Seven International) that defines how health data should be structured and shared between systems. J4H uses the R4 version — the current stable release.

Core idea: every piece of clinical data — a patient record, a lab result, a diagnosis — is a resource expressed as a JSON document. Resources are fetched via plain HTTP GET requests. No proprietary protocol, no special driver.

The resources J4H uses

👤 Patient

Name, gender, date of birth, FHIR ID. Used to identify who the diary entries belong to.

🩺 Condition

Diagnoses and clinical problems — e.g. "Type 2 Diabetes". Includes onset date and clinical status.

📏 Observation (vital-signs)

Blood pressure, heart rate, body weight, temperature. Each observation has a date, value, and unit.

🧪 Observation (laboratory)

Cholesterol panels (total, HDL, LDL, triglycerides). Fetched with LOINC codes 2093-3, 2085-9, 35200-5, 18262-6.

What a FHIR response looks like

Every search returns a Bundle — a wrapper containing a list of matching resources. Each item in bundle["entry"] holds one resource.

# A FHIR Bundle response (simplified) { "resourceType": "Bundle", "total": 2, "entry": [ { "resource": { "resourceType": "Patient", "id": "pat-001", "name": [{ "given": ["Jane"], "family": "Smith" }], "gender": "female", "birthDate": "1980-04-15" } } ] }
FHIR R4 Server Patient Condition Observation
The three FHIR resource types used by J4H
🎯
Why J4H Connects to FHIR
Strategy: augmenting a personal diary with clinical records

A personal health diary captures what the patient experiences — pain level, symptoms, location, context. A FHIR server captures what the clinician records — diagnoses, lab values, measurements. J4H connects the two.

Without FHIR

The AI summary only knows what the patient wrote in their diary entries. No clinical context. The AI has to guess at conditions.

With FHIR

The vitals page shows real clinical measurements over time. The health record page lists confirmed diagnoses and observations.

The integration strategy

  • Read-only — J4H pulls data from FHIR servers; it never writes back. No write permissions needed.
  • Patient-scoped — you search for and select a patient once; that ID is stored in the local settings table. All subsequent FHIR calls use that ID.
  • Multi-server aware — the server is configured via environment variable, making it easy to point at a test server locally and GTRI on Heroku.
  • Graceful degradation — FHIR calls time out after 10 seconds. If the server is down, the rest of the app still works.
  • Structured parsing — raw FHIR JSON is parsed into clean Python dicts before being returned to the frontend, insulating the UI from FHIR schema changes.
Long-term vision: as J4H evolves toward the Health Memory Association model, FHIR becomes the bridge to provider systems. A doctor viewing a patient's record can pull the same diary-plus-clinical data that J4H has assembled — because FHIR is the universal language healthcare systems speak.
🔌
The Connection: fhir_service.py
FHIRService class, server config, and HTTP calls

Server configuration

Four FHIR servers are pre-configured as a dict. The one actually used is selected by the FHIR_SERVER environment variable. On Heroku this is set to hapi-gtri. Locally it defaults to the public HAPI sandbox.

# fhir_service.py — server registry FHIR_SERVERS = { 'hapi-public': 'https://hapi.fhir.org/baseR4', 'hapi-gtri': os.getenv('GTRI_FHIR_BASE_URL', 'https://hapifhir.heat.icl.gtri.org/fhir'), 'smart-sandbox': 'https://launch.smarthealthit.org/v/r4/fhir', 'custom': os.getenv('FHIR_BASE_URL', 'https://hapi.fhir.org/baseR4') } def get_fhir_base_url(): server_type = os.getenv('FHIR_SERVER', 'hapi-public') return FHIR_SERVERS.get(server_type, FHIR_SERVERS['hapi-public'])

The FHIRService class

All HTTP communication lives in one class. The constructor sets the base URL and the required FHIR content-type headers. Every method builds a URL, fires a requests.get() call with a 10-second timeout, then parses the response.

# fhir_service.py — class init and patient search class FHIRService: def __init__(self, base_url=None): self.base_url = base_url or get_fhir_base_url() self.headers = { 'Accept': 'application/fhir+json', 'Content-Type': 'application/fhir+json' } def search_patients(self, name=None, limit=10): params = {'_count': limit} if name: params['name'] = name response = requests.get( f"{self.base_url}/Patient", params=params, headers=self.headers, timeout=10 ) response.raise_for_status() bundle = response.json() patients = [] for entry in bundle.get('entry', []): patients.append(self._parse_patient(entry['resource'])) return patients

Fetching vitals — two separate calls

Vitals and cholesterol live in different Observation categories on FHIR servers, so get_patient_vitals() fires two HTTP requests and merges the results. Blood pressure is special: it stores systolic and diastolic as sub-components within one Observation resource, so the parser loops over resource["component"].

# fhir_service.py — fetching vitals + cholesterol def get_patient_vitals(self, patient_id): vitals = {} ## call 1 — vital signs (BP, HR, weight, temp…) response = requests.get( f"{self.base_url}/Observation", params={'patient': patient_id, '_count': 200, '_sort': 'date', 'category': 'vital-signs'}, headers=self.headers, timeout=10 ) for entry in response.json().get('entry', []): self._add_to_vitals(vitals, entry['resource']) ## call 2 — cholesterol panels (LOINC codes) response = requests.get( f"{self.base_url}/Observation", params={'patient': patient_id, '_count': 100, 'category': 'laboratory', 'code': '2093-3,2085-9,35200-5,18262-6'}, headers=self.headers, timeout=10 ) for entry in response.json().get('entry', []): self._add_to_vitals(vitals, entry['resource']) return vitals def _add_to_vitals(self, vitals, resource): ## blood pressure: read systolic/diastolic from component array if 'component' in resource: for comp in resource['component']: comp_name = comp['code'].get('text') or \ comp['code']['coding'][0]['display'] value = comp.get('valueQuantity', {}).get('value') unit = comp.get('valueQuantity', {}).get('unit', '') if value is not None: vitals.setdefault(comp_name, []).append( {'date': date, 'value': value, 'unit': unit}) return ## everything else: single valueQuantity value = resource.get('valueQuantity', {}).get('value') if value is not None: vitals.setdefault(name, []).append( {'date': date, 'value': value, 'unit': unit})
Browser / JS fetch() Flask Route /api/fhir/vitals .get_patient_vitals() FHIRService fhir_service.py HTTP GET FHIR Server GTRI / HAPI parsed vitals dict → JSON response
Request path: browser → Flask route → FHIRService → FHIR server → back
🛤️
Flask Routes That Hook It Up
How app.py exposes FHIR data as a REST API

Startup: one shared instance

At the top of app.py a single FHIRService object is created when the server starts. Every route handler reuses it — no reconnection overhead on each request.

# app.py — module-level init (line 68) from fhir_service import FHIRService fhir = FHIRService() # reads FHIR_SERVER env var at startup

The FHIR API routes

Method Route What it does
GET /api/fhir/patients/search Search patients by name — calls fhir.search_patients()
GET /api/fhir/patient/<id> Full patient record: demographics + conditions + observations
POST /api/fhir/patient/select Stores chosen patient_id + name in the local settings table
GET /api/fhir/patient/current Returns the currently selected patient (used by the SPA on load)
GET /api/fhir/vitals Grouped vitals dict for the selected patient — powers the Vitals page charts
GET /api/fhir/health-record Combined patient + conditions + observations for the Health Record page
GET /api/fhir/server/status Tests connectivity to the configured FHIR server (used by Admin dashboard)

Patient selection — stored in the settings table

When the user selects a patient on the Patient Data page, the frontend POSTs to /api/fhir/patient/select. Flask verifies the patient exists on the FHIR server, then writes two rows to the local settings table. Every subsequent FHIR call reads from there — no session cookies needed.

# app.py — /api/fhir/patient/select @app.route('/api/fhir/patient/select', methods=['POST']) def select_fhir_patient(): patient_id = request.json.get('patient_id') patient = fhir.get_patient(patient_id) ## verify exists if not patient: return jsonify({'error': 'Patient not found'}), 404 db.set_setting('selected_patient_id', patient_id) db.set_setting('selected_patient_name', patient['name']) return jsonify({'success': True, 'patient': patient}) # app.py — /api/fhir/vitals (uses stored patient_id) @app.route('/api/fhir/vitals', methods=['GET']) def get_fhir_vitals(): patient_id = db.get_setting('selected_patient_id') if not patient_id: return jsonify({'error': 'No patient selected'}), 404 vitals = fhir.get_patient_vitals(patient_id) return jsonify(vitals)
🔬
Parsing FHIR Resources
Converting raw FHIR JSON into clean Python dicts

Raw FHIR JSON is deeply nested and full of optional fields. Each _parse_*() method extracts what J4H needs and returns a flat dict. The rest of the app never touches raw FHIR JSON.

Patient parsing

# fhir_service.py — _parse_patient() def _parse_patient(self, resource): patient = { 'id': resource.get('id'), 'name': 'Unknown', 'gender': resource.get('gender', 'unknown'), 'birthDate': resource.get('birthDate'), 'age': None } ## name is a list of HumanName objects; take the first one if resource.get('name'): n = resource['name'][0] parts = list(n.get('given', [])) + ([n['family']] if 'family' in n else []) patient['name'] = ' '.join(parts) ## age is not stored in FHIR — derive it from birthDate if patient['birthDate']: patient['age'] = datetime.now().year - int(patient['birthDate'][:4]) return patient

Condition parsing

# fhir_service.py — _parse_condition() def _parse_condition(self, resource): condition = { 'id': resource.get('id'), 'code': 'Unknown condition', 'status': resource.get('clinicalStatus', {}).get('text', 'unknown'), 'onset': None } ## code.text is human-readable; fall back to coding[0].display if 'code' in resource: condition['code'] = (resource['code'].get('text') or resource['code'].get('coding', [{}])[0].get('display', 'Unknown')) if 'onsetDateTime' in resource: condition['onset'] = resource['onsetDateTime'][:10] ## YYYY-MM-DD return condition
Why parse at the service layer? FHIR servers sometimes return code.text, sometimes only code.coding[0].display. The parser handles both cases once. The rest of the app just reads condition["code"] — a plain string.
🔄
End-to-End Data Flow
From patient search to vitals chart — step by step
  1. User visits /patient. Types a name in the search box. JS calls GET /api/fhir/patients/search?name=Smith. Flask calls fhir.search_patients(name="Smith"), which hits GET {base_url}/Patient?name=Smith&_count=20. The FHIR server returns a Bundle; Flask parses and returns a list of patient dicts.
  2. User selects a patient. JS POSTs { patient_id: "pat-001" } to /api/fhir/patient/select. Flask verifies the patient exists, then stores selected_patient_id = "pat-001" and selected_patient_name = "Jane Smith" in the local settings table.
  3. SPA loads the home page. loadCurrentPatient() calls GET /api/fhir/patient/current. Flask reads selected_patient_id from the settings table, fetches the patient from the FHIR server, and returns it. The SPA sets the global currentPatientName shown in the header.
  4. User visits /vitals. JS calls GET /api/fhir/vitals. Flask reads selected_patient_id from settings and calls fhir.get_patient_vitals(patient_id) — two HTTP calls (vital-signs + laboratory) that return a grouped dict like { "Systolic BP": [{date, value, unit}, …], … }. The vitals page renders this as Chart.js line charts.
  5. User visits /health-record. GET /api/fhir/health-record fetches patient + conditions + observations in one Flask route and returns them bundled as a single JSON response.
1. Search /patient page 2. Select POST /patient/select 3. Stored settings table (DB) 4. SPA Loads /patient/current 5. Vitals Page GET /fhir/vitals 6. Health Record GET /fhir/health-record selected_patient_id drives all reads
Patient selection stores the ID; every downstream FHIR call reads it from the DB
🔐
SMART on FHIR — OAuth 2.0 Layer
Scaffolded for future authenticated provider integration

The current integration uses open FHIR servers (GTRI, HAPI public) that require no authentication. Real EHR systems — Epic, Cerner, etc. — require SMART on FHIR, which adds an OAuth 2.0 authorization layer on top of FHIR.

Current (open access)

No auth headers. Works with public test servers. Patient data is synthetic / de-identified. Suitable for development and education.

SMART on FHIR (scaffolded)

OAuth 2.0 flow: app requests a launch token, redirects to the EHR's auth server, receives an access token, attaches it as Authorization: Bearer <token>.

What's already in the codebase

  • /launch — receives the SMART launch token + ISS URL from the EHR, starts the OAuth flow
  • /auth/callback — OAuth redirect URI; exchanges authorization code for access token
  • /smart-info — informational page explaining the SMART launch sequence
  • smart-sandbox entry in FHIR_SERVERS — points to SMART Health IT test launcher
Current status: the SMART auth endpoints are scaffolded but not fully wired. The app works end-to-end today using open FHIR servers. Completing SMART would allow J4H to connect to a real hospital's EHR with the patient's consent — the next step toward the full Health Memory Association vision.
# app.py — SMART launch endpoint (scaffold) @app.route('/launch') def app_launch(): launch_token = request.args.get('launch') iss = request.args.get('iss') ## FHIR server URL from EHR auth_url, state = smart_auth.get_authorization_url_with_launch(launch_token) session['oauth_state'] = state return redirect(auth_url) ## redirect user to EHR login page

FHIR is the plumbing of modern healthcare interoperability.

J4H uses it to do something simple but powerful: place clinical context next to personal experience. When an AI summarizes your diary entries for your cardiologist, it can reference real cholesterol readings and blood pressure trends — not just what you remember to write down.

That's the integration strategy in one sentence: your story, grounded in your data.