FHIR Resources: Patient, Observation, and Beyond
By David Le -- Part 5 of the FhirHub Series
FHIR (Fast Healthcare Interoperability Resources) defines a standard way to represent clinical data. FhirHub implements several FHIR R4 resource types, each mapped to typed DTOs and exposed through REST endpoints. This post covers the resources we use, the LOINC codes that identify clinical measurements, and how the API translates between FHIR and the frontend.
FHIR R4 Resources in FhirHub
FhirHub works with these FHIR resource types:
| Resource | Purpose | Example |
|---|---|---|
| Patient | Demographics and identifiers | Name, DOB, gender, contact info |
| Observation | Vital signs and lab results | Blood pressure, glucose level |
| Condition | Diagnoses and health problems | Hypertension, Type 2 Diabetes |
| MedicationRequest | Medication orders | Lisinopril 10mg daily |
| DiagnosticReport | Lab report panels | Basic Metabolic Panel |
| Encounter | Patient visits | Office visit, ER admission |
| Procedure | Clinical procedures | Blood draw, ECG |
| Immunization | Vaccination records | COVID-19 vaccine |
| AllergyIntolerance | Allergies | Penicillin allergy |
| DocumentReference | Clinical documents | Discharge summary PDF |
The API Layer
The PatientsController exposes RESTful endpoints for clinical data:
[ApiController]
[Route("api/patients")]
[Authorize]
public class PatientsController : ControllerBase
{
[HttpGet] // GET /api/patients
[HttpGet("{id}")] // GET /api/patients/{id}
[HttpPost] // POST /api/patients
[HttpGet("{id}/vitals")] // GET /api/patients/{id}/vitals
[HttpGet("{id}/vitals/chart")] // GET /api/patients/{id}/vitals/chart
[HttpPost("{id}/vitals")] // POST /api/patients/{id}/vitals
[HttpGet("{id}/conditions")] // GET /api/patients/{id}/conditions
[HttpPost("{id}/conditions")] // POST /api/patients/{id}/conditions
[HttpGet("{id}/medications")] // GET /api/patients/{id}/medications
[HttpPost("{id}/medications")] // POST /api/patients/{id}/medications
[HttpGet("{id}/labs")] // GET /api/patients/{id}/labs
[HttpPost("{id}/labs/orders")] // POST /api/patients/{id}/labs/orders
[HttpGet("{id}/timeline")] // GET /api/patients/{id}/timeline
}
Every endpoint is protected with [Authorize] and applies specific policies. Write endpoints also have rate limiting:
[HttpPost("{id}/vitals")]
[Authorize(Policy = AuthorizationPolicies.CanWriteVitals)]
[EnableRateLimiting("WriteOperations")]
public async Task<IActionResult> RecordVitals(
string id,
[FromBody] RecordVitalsRequest request,
CancellationToken ct)
{
var result = await _patientService.RecordVitalsAsync(id, request, ct);
return Created($"/api/patients/{id}/vitals", result);
}
LOINC Codes
LOINC (Logical Observation Identifiers Names and Codes) is the standard coding system for clinical observations. FhirHub maps vitals to specific LOINC codes:
// frontend/src/lib/clinical-rules.ts
export const LOINC_CODES = {
SYSTOLIC_BP: "8480-6",
DIASTOLIC_BP: "8462-4",
BLOOD_PRESSURE: "85354-9", // Blood pressure panel
HEART_RATE: "8867-4",
BODY_TEMPERATURE: "8310-5",
BODY_WEIGHT: "29463-7",
RESPIRATORY_RATE: "9279-1",
OXYGEN_SATURATION: "2708-6",
} as const;
Each vital sign in the system is identified by its LOINC code rather than a display name. This ensures interoperability -- any FHIR-compliant system will recognize 8480-6 as systolic blood pressure.
FHIR to DTO Mapping
Raw FHIR resources are verbose and deeply nested. The API maps them to flattened DTOs before sending to the frontend. For example, a FHIR Patient resource with extensions, identifiers, and contact points gets mapped to a clean DTO with just the fields the UI needs.
The mapping happens in the repository layer using the Hl7.Fhir.R4 library:
HAPI FHIR → Hl7.Fhir.R4 objects → DTOs → JSON response
This separation means:
- The frontend never deals with FHIR complexity
- DTO shapes can evolve independently of the FHIR standard
- Validation happens at both the FHIR level (via HAPI) and the API level (via FluentValidation)
Additional Controllers
Beyond patient data, FhirHub has controllers for:
- DashboardController -- Aggregate metrics and system overview
- ClinicalController -- Cross-patient clinical data
- ExportsController -- Bulk data export management
- UsersController -- Keycloak user administration
- AuditLogController -- Activity audit trail
Each follows the same pattern: thin controller, service layer for business logic, repository for HAPI FHIR communication.
Creating FHIR CodeableConcepts
When recording new clinical data, the frontend constructs proper FHIR CodeableConcepts:
// frontend/src/lib/clinical-rules.ts
export function createVitalCode(
loincCode: string,
displayName: string
): CodeableConceptDTO {
return {
coding: [
{
system: "http://loinc.org",
code: loincCode,
display: displayName,
},
],
text: displayName,
};
}
This ensures every observation created through FhirHub includes proper LOINC coding, making the data portable across FHIR-compliant systems.
What's Next
In Part 6, we'll look at how vitals data gets visualized with interactive Recharts line charts, including reference range overlays and multi-vital selection.