Seldom do contemporary healthcare applications operate independently.
Data from an Electronic Health Record (EHR) may be required by a patient portal. A healthcare system and a remote patient monitoring application might need to share observations. Patient demographics, appointments, prescription drugs, or diagnostic data may be required by a telehealth platform.
Healthcare systems can represent and communicate data in different ways, which presents a difficulty.
Fast Healthcare Interoperability Resources, or FHIR, offers a standardized method for sharing medical data using RESTful APIs and structured resources.
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
+---- MedicationRequest
This 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/123
to retrieve a patient.
It could then request observations belonging to that patient:
GET /Observation?patient=123
A 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 Server
There 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 HealthcareFhirApi
ASP.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.R4
The Firely .NET SDK provides .NET classes corresponding to FHIR resources.
For example:
Patient
Observation
Condition
Encounter
MedicationRequest
Instead 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/123
causes 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/observations
The 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 Resource
This can be appropriate if the consuming application understands FHIR.
Another approach is:
FHIR Server
|
v
FHIR Resource
|
v
Transformation Layer
|
v
Application DTO
For 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 -> 403
Production 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
Observation
It should not automatically receive permissions for:
MedicationRequest
Condition
DiagnosticReport
DocumentReference
Encounter
Request 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 identifier
Do 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 = -500
The 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 Rules
Do 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 API
This 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 OK
Unknown Patient
FHIR server returns a not-found response.
Expected result:
404 Not Found
FHIR 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 Forbidden
These 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
By offering defined resources and API standards for sharing healthcare data, FHIR makes healthcare interoperability more accessible to developers.
In this post, we used C# and ASP.NET Core to build the fundamental framework of a healthcare API that is suitable for FHIR. We set up a dedicated FHIR service, installed the FHIR.NET SDK, fetched Patient and Observation resources, made them available via API endpoints, and looked at the extra security and architecture considerations needed for actual healthcare systems.
Although the example is purposefully straightforward, the same concepts may be used to bigger interoperability systems, clinical dashboards, telehealth apps, patient portals, remote monitoring platforms, and mobile health applications.
The key takeaway is that more is needed for production healthcare interoperability than just establishing a connection between an HTTP client and an FHIR endpoint. Standardized healthcare data interchange, secure API design, cautious authorization, validation, auditability, resilient architecture, and suitable healthcare-domain rules are all necessary components of a strong solution.
Best ASP.NET Core 11.0 Hosting
The feature and reliability are the most important things when choosing a good ASP.NET Core 11.0 hosting. HostForLIFE is the leading provider of Windows hosting and affordable ASP.NET Core , their servers are optimized for PHP web applications such as the latest ASP.NET Core 11.0 version. The performance and the uptime of the ASP.NET CoreĀ hosting service are excellent, and the features of the web hosting plan are even greater than what many hosting providers ask you to pay for. At HostForLIFE.eu, customers can also experience fast ASP.NET Core hosting. The company invested a lot of money to ensure the best and fastest performance of the datacenters, servers, network and other facilities. Its data centers are equipped with top equipment like cooling system, fire detection, high-speed Internet connection, and so on. That is why HostForLIFE.eu guarantees 99.9% uptime for ASP.NET Core . And the engineers do regular maintenance and monitoring works to assure its ASP.NET CoreĀ hosting are security and always up.

