Modern healthcare applications rarely work in isolation.
A patient portal may need data from an Electronic Health Record (EHR). A remote patient monitoring application may need to exchange observations with a clinical system. A telehealth platform may need patient demographics, appointments, medications, or diagnostic information.
The challenge is that healthcare systems can represent and exchange data differently.
FHIR, or Fast Healthcare Interoperability Resources, provides a standardized approach for exchanging healthcare information through structured resources and RESTful APIs.
For .NET developers, FHIR can be integrated into healthcare applications using ASP.NET Core and libraries such as the Firely .NET SDK.
In this article, we will build a simple FHIR-ready healthcare API using C# and ASP.NET Core. We will look at:
What FHIR means from a developer's perspective
How FHIR resources are structured
How to create an ASP.NET Core healthcare API
How to use the Firely .NET SDK
How to retrieve patient information from a FHIR server
How to retrieve clinical observations
How to structure the application cleanly
Important authentication and security considerations
Common mistakes developers should avoid
The examples are designed for learning and architecture demonstration. Production healthcare applications require additional security, validation, authorization, auditing, testing, and regulatory review.
What Is FHIR?
FHIR stands for Fast Healthcare Interoperability Resources.
It is a healthcare interoperability standard developed by HL7 for representing and exchanging healthcare information electronically.
Instead of treating an entire medical record as one large data structure, FHIR organizes healthcare information into individual resources.
Examples include:
Patient
Practitioner
Observation
Condition
Encounter
MedicationRequest
AllergyIntolerance
Appointment
DiagnosticReport
CarePlan
Each resource represents a particular healthcare concept.
For example, a Patient resource can represent demographic information, while an Observation resource can represent a blood-pressure reading, laboratory value, temperature, or other clinical measurement.
A simplified interaction might look like this:
Patient Application
|
v
ASP.NET Core API
|
v
FHIR Integration Service
|
v
FHIR Server
|
+---- Patient
+---- Observation
+---- Condition
+---- MedicationRequestThis structure allows applications to exchange healthcare information through well-defined resource models.
Why Use FHIR in a Healthcare Application?
Imagine you are developing a patient application that needs information from an EHR.
Without a common interoperability model, your application may need a custom integration for every healthcare system.
FHIR creates a standard interface around healthcare information.
A healthcare application could request:
GET /Patient/123to retrieve a patient.
It could then request observations belonging to that patient:
GET /Observation?patient=123A real implementation may include additional profiles, terminology requirements, authorization scopes, identifiers, search parameters, and implementation-guide rules.
However, the resource-based model gives developers a common foundation.
Our Example Architecture
For this example, we will place our ASP.NET Core API between the application and the FHIR server.
Web / Mobile / Clinical Application
|
v
ASP.NET Core API
|
v
Healthcare Services
|
v
FHIR Client Layer
|
v
FHIR ServerThere are several advantages to this approach.
The frontend does not need to know the details of every FHIR integration.
The API can also provide a central location for:
Authentication
Authorization
Request validation
Data transformation
Audit logging
Error handling
Rate limiting
Business rules
Monitoring
It also avoids putting healthcare-integration logic directly inside controllers.
Step 1: Create the ASP.NET Core Web API
Create a project from the command line:
dotnet new webapi -n HealthcareFhirApi
cd HealthcareFhirApiASP.NET Core supports both controller-based APIs and Minimal APIs.
For this example, we will use controllers because they make the separation between the API layer and healthcare integration layer easy to demonstrate.
Step 2: Install the FHIR .NET SDK
We will work with FHIR R4 in this example.
Install the appropriate package:
dotnet add package Hl7.Fhir.R4The Firely .NET SDK provides .NET classes corresponding to FHIR resources.
For example:
Patient
Observation
Condition
Encounter
MedicationRequestInstead of manually parsing healthcare JSON into custom models, we can work with typed FHIR objects.
Add the namespaces where required:
using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;Step 3: Add the FHIR Server Configuration
Do not hardcode environment-specific FHIR endpoints throughout the application.
Add a configuration section to appsettings.json.
{
"Fhir": {
"ServerUrl": "https://your-fhir-server.example/fhir"
}
}For real healthcare environments, credentials and sensitive configuration should be handled through an appropriate secrets-management mechanism rather than committed to source control.
Create a configuration model:
public class FhirOptions
{
public string ServerUrl { get; set; } = string.Empty;
}Register the configuration in Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<FhirOptions>(
builder.Configuration.GetSection("Fhir"));
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();We now have the basic ASP.NET Core API structure.
Step 4: Create a FHIR Service
Controllers should not be responsible for configuring FHIR clients and implementing healthcare-integration logic.
Create an interface:
using Hl7.Fhir.Model;
public interface IFhirService
{
Task<Patient?> GetPatientAsync(string id);
Task<IReadOnlyList<Observation>>
GetPatientObservationsAsync(string patientId);
}Next, create the implementation.
using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
using Microsoft.Extensions.Options;
public class FhirService : IFhirService
{
private readonly string _serverUrl;
public FhirService(IOptions<FhirOptions> options)
{
_serverUrl = options.Value.ServerUrl;
}
private FhirClient CreateClient()
{
return new FhirClient(_serverUrl);
}
public async Task<Patient?> GetPatientAsync(string id)
{
var client = CreateClient();
try
{
return await client.ReadAsync<Patient>(
$"Patient/{id}");
}
catch (FhirOperationException ex)
when (ex.Status == System.Net.HttpStatusCode.NotFound)
{
return null;
}
}
public async Task<IReadOnlyList<Observation>>
GetPatientObservationsAsync(string patientId)
{
var client = CreateClient();
var bundle =
await client.SearchAsync<Observation>(
new[]
{
$"patient=Patient/{patientId}"
});
if (bundle?.Entry == null)
{
return Array.Empty<Observation>();
}
return bundle.Entry
.Where(entry => entry.Resource is Observation)
.Select(entry => (Observation)entry.Resource)
.ToList();
}
}This service performs two operations.
The first reads an individual Patient resource.
The second searches for Observation resources associated with a patient.
The important architectural point is that the controller does not need to know how FHIR communication works.
Step 5: Register the FHIR Service
Register the service with ASP.NET Core dependency injection.
Update Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<FhirOptions>(
builder.Configuration.GetSection("Fhir"));
builder.Services.AddScoped<IFhirService, FhirService>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();The integration service can now be injected into API controllers.
Step 6: Create the Patient Controller
Create a controller for retrieving patient information.
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/patients")]
public class PatientsController : ControllerBase
{
private readonly IFhirService _fhirService;
public PatientsController(IFhirService fhirService)
{
_fhirService = fhirService;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetPatient(
string id)
{
var patient =
await _fhirService.GetPatientAsync(id);
if (patient == null)
{
return NotFound();
}
return Ok(patient);
}
}Calling:
GET /api/patients/123causes our ASP.NET Core application to request the corresponding Patient resource from the FHIR server.
Step 7: Retrieve Clinical Observations
Now add an endpoint for observations.
[HttpGet("{id}/observations")]
public async Task<IActionResult> GetObservations(
string id)
{
var observations =
await _fhirService
.GetPatientObservationsAsync(id);
return Ok(observations);
}A request might look like:
GET /api/patients/123/observationsThe resulting FHIR resources may contain measurements such as:
Blood pressure
Heart rate
Body temperature
Oxygen saturation
Laboratory values
Body weight
The exact interpretation depends on the coding systems and profiles used by the connected healthcare environment.
Understanding a FHIR Observation
A simplified observation can conceptually contain:
{
"resourceType": "Observation",
"status": "final",
"code": {
"text": "Heart rate"
},
"subject": {
"reference": "Patient/123"
},
"valueQuantity": {
"value": 72,
"unit": "beats/minute"
}
}Developers should not assume that every Observation uses valueQuantity.
FHIR observations can represent different types of values.
Production applications should interpret resources according to the relevant FHIR profiles and implementation guides.
Should We Return Raw FHIR Resources?
It depends on the purpose of the API.
One option is:
FHIR Server
|
v
ASP.NET Core API
|
v
Raw FHIR ResourceThis can be appropriate if the consuming application understands FHIR.
Another approach is:
FHIR Server
|
v
FHIR Resource
|
v
Transformation Layer
|
v
Application DTOFor example:
public record PatientSummaryDto(
string Id,
string? FirstName,
string? LastName,
string? Gender,
string? BirthDate);You can map a FHIR Patient into an application-specific representation.
private static PatientSummaryDto MapPatient(
Patient patient)
{
var name = patient.Name.FirstOrDefault();
return new PatientSummaryDto(
patient.Id,
name?.Given.FirstOrDefault(),
name?.Family,
patient.Gender?.ToString(),
patient.BirthDate);
}This approach prevents the frontend from becoming tightly coupled to the entire FHIR data model.
However, transformation also introduces another model that developers must maintain.
The right choice depends on the system architecture.
Add Error Handling
Healthcare integrations depend on external systems, so errors should be expected.
Examples include:
FHIR server unavailable
Authentication failure
Resource not found
Validation error
Unsupported operation
Timeout
Invalid search parameter
Avoid exposing raw exceptions to applications.
ASP.NET Core applications can implement centralized exception handling using middleware or an exception handler.
A production API might translate an upstream FHIR problem into an appropriate API response while recording technical details securely for operations teams.
Be careful with logging.
Clinical resources may contain protected or sensitive healthcare information. Logging complete request and response payloads without a deliberate policy can expose patient information.
Authentication Is Different from Authorization
Security is particularly important for healthcare APIs.
Authentication answers:
Who is making this request?Authorization answers:
What is this user or application
allowed to access?A valid access token should not automatically mean that a user can access every patient.
For example:
Authenticated User
|
v
Access Token
|
v
ASP.NET Core API
|
v
Authorization Check
|
+---- Allowed -> Continue
|
+---- Denied -> 403Production FHIR environments frequently use OAuth 2.0-based authorization patterns, and healthcare ecosystems may use SMART on FHIR for authorization and application launch scenarios.
The exact approach depends on the EHR, FHIR server, organization, deployment model, and implementation guide.
Never Hardcode Healthcare Credentials
Avoid code like:
var clientSecret = "my-production-secret";Secrets can accidentally appear in:
Git repositories
CI/CD logs
screenshots
configuration backups
developer machines
deployment artifacts
Use an appropriate secret-management solution.
Depending on the environment, this could include:
Azure Key Vault
AWS Secrets Manager
HashiCorp Vault
Kubernetes Secrets with suitable controls
A managed identity or workload identity approach
The best credential is often one that your application does not need to store directly.
Apply Least Privilege
Suppose an application only needs to read:
Patient
ObservationIt should not automatically receive permissions for:
MedicationRequest
Condition
DiagnosticReport
DocumentReference
EncounterRequest only the permissions that the application actually requires.
Limiting access reduces the consequences of compromised credentials and programming mistakes.
Consider Auditability
Healthcare applications often need to answer questions such as:
Who accessed patient information?
When did they access it?
What operation did they perform?
Which system initiated the request?
Was the request successful?An application therefore needs an audit strategy rather than ordinary debugging logs alone.
A simplified audit event could include:
Timestamp
User or client identifier
Operation
Resource type
Resource identifier
Result
Correlation identifierDo not automatically store full clinical payloads in the audit record.
Audit design should balance traceability with data minimization.
Add Validation at the Boundary
Receiving valid JSON does not mean you have received valid healthcare data.
Consider an Observation representing body temperature.
Technically, the API could receive:
Temperature = -500The value might be syntactically valid while being meaningless for the application.
Different layers of validation may therefore be required:
Transport Validation
|
v
FHIR Structural Validation
|
v
Profile Validation
|
v
Terminology Validation
|
v
Application Business RulesDo not treat all validation as the same problem.
FHIR profiles and healthcare implementation guides may place additional constraints on base resources.
Think About Terminology
Healthcare information often relies on standardized coding systems.
Examples can include:
LOINC
SNOMED CT
ICD
RxNorm
A UI label such as:
"Heart Rate"is useful for a human but does not necessarily provide enough semantic precision for interoperable software.
Healthcare integrations often require systems to understand the code, coding system, version, and relevant value set.
This is one reason interoperability involves more than simply exchanging JSON.
Do Not Assume Every FHIR Server Behaves Identically
FHIR defines standardized capabilities, but individual implementations can differ.
A server may support different:
FHIR versions
Search parameters
profiles
extensions
operations
terminology requirements
authorization models
Before integrating with a FHIR server, examine its CapabilityStatement and relevant implementation guide.
An architecture should therefore avoid scattering assumptions about one server throughout the codebase.
Keeping integration logic behind a service boundary makes future changes easier.
A Better Production Architecture
Our tutorial implementation is intentionally small.
A larger application may evolve toward this structure:
Client Applications
|
v
ASP.NET Core API
|
+------------------+------------------+
| | |
v v v
Authentication Authorization Validation
| | |
+------------------+------------------+
|
v
Application Layer
|
v
Healthcare Domain Layer
|
v
FHIR Integration
|
+---------------+---------------+
| |
v v
FHIR Server Other Systems
|
+------------+------------+
| |
v v
Lab API Device APIThis separates healthcare interoperability concerns from core application logic.
It also creates better boundaries for testing.
Testing the Integration
Do not wait for production connectivity before testing your FHIR layer.
Create tests for scenarios such as:
Valid Patient
FHIR server returns an existing Patient resource.
Expected result:
200 OKUnknown Patient
FHIR server returns a not-found response.
Expected result:
404 Not FoundFHIR Server Failure
FHIR service becomes unavailable.
Expected result:
The API returns a controlled error without exposing internal details.
Invalid FHIR Data
A response violates assumptions used by the application.
Expected result:
The application detects the condition rather than silently processing invalid information.
Unauthorized Access
A user attempts to retrieve a patient outside their allowed scope.
Expected result:
403 ForbiddenThese scenarios are just as important as testing the successful path.
Common FHIR Integration Mistakes
1. Treating FHIR as a Database Schema
FHIR is an interoperability specification.
Your internal database does not necessarily need to replicate every FHIR resource.
2. Ignoring Profiles
A base FHIR resource may be constrained by an implementation guide or organizational profile.
3. Hardcoding Resource Assumptions
Different healthcare environments can use different profiles, extensions, and coding conventions.
4. Returning Too Much Patient Data
Only retrieve and expose information required for the application use case.
5. Logging Complete FHIR Payloads
FHIR resources may contain highly sensitive patient information.
6. Mixing Integration Logic with Controllers
Keep healthcare connectivity behind a dedicated service or integration layer.
7. Ignoring Authorization Context
Knowing that a Patient resource exists does not mean every authenticated user should be able to retrieve it.
8. Building Only for the Happy Path
Network failures, incorrect data, authentication failures, timeouts, and unsupported operations are normal integration scenarios.
Practical Use Cases
Once this foundation is in place, the same architecture can support several types of healthcare applications.
Patient Portals
Retrieve demographics, appointments, results, medication information, and related patient data.
Remote Patient Monitoring
Exchange observations generated through connected monitoring workflows.
Telehealth Applications
Connect virtual-care applications with patient and encounter information.
Clinical Dashboards
Aggregate relevant healthcare resources for authorized clinical users.
Healthcare Mobile Applications
Provide controlled access to healthcare information through mobile experiences.
Care Management Systems
Combine conditions, observations, care plans, encounters, and related clinical information.
FHIR does not build these applications for us. It provides a standardized foundation for exchanging the healthcare information they depend on.
Key Takeaways
Building a healthcare API is not only an API-development problem.
Developers need to think about:
Healthcare data models
Interoperability
Authentication
Authorization
Clinical terminology
Validation
Privacy
Auditability
External-system failures
Data minimization
ASP.NET Core provides a strong foundation for building APIs, while the Firely .NET SDK gives C# developers typed models and utilities for working with FHIR resources.
The most maintainable architecture is usually one where FHIR-specific concerns are isolated behind a well-defined integration layer rather than spread throughout the application.
Conclusion
FHIR makes healthcare interoperability more approachable for developers by providing standardized resources and API patterns for exchanging healthcare information.
In this article, we created the basic structure of a FHIR-ready healthcare API using ASP.NET Core and C#. We installed the FHIR .NET SDK, created a dedicated FHIR service, retrieved Patient and Observation resources, exposed them through API endpoints, and examined the additional security and architectural concerns required for real healthcare systems.
The sample is intentionally simple, but the same principles can be expanded into patient portals, remote monitoring platforms, telehealth applications, clinical dashboards, healthcare mobile applications, and larger interoperability platforms.
The important lesson is that production healthcare interoperability requires more than connecting an HTTP client to a FHIR endpoint. A robust solution must combine standardized healthcare data exchange with secure API design, careful authorization, validation, auditability, resilient architecture, and appropriate healthcare-domain rules.

Join the conversation! Your thoughts help the community grow.