Building a SMART on FHIR App in Java: A Step-by-Step Guide
Introduction
Welcome to the latest article in my series on FHIR Programming. In this tutorial, we will explore how to build a SMART on FHIR application using Java. SMART on FHIR combines the SMART (Substitutable Medical Applications, Reusable Technologies) framework with FHIR to create a robust platform for developing healthcare applications that can securely integrate with electronic health record (EHR) systems.
SMART on FHIR provides a standardized approach for authentication, authorization, and data access, enabling your applications to seamlessly connect with different healthcare systems without requiring custom integration code for each system. This interoperability is a game-changer for healthcare software development, allowing developers to focus on building valuable features rather than wrestling with integration challenges.
Deep Dive into SMART on FHIR Architecture
Before diving into implementation, let's understand the architecture and components that make SMART on FHIR work.
The OAuth2 Authorization Flow in Detail
SMART on FHIR uses OAuth2 with specific extensions for healthcare. Here's a detailed breakdown of the authorization flow:
SMART App Launch Framework Components
1. Authorization Server
- Handles user authentication and consent
- Issues authorization codes and access tokens
- Validates client credentials and redirect URIs
- May be integrated with the EHR or standalone (e.g., Keycloak, Auth0)
2. Resource Server (FHIR Server)
- Hosts the FHIR API endpoints
- Validates access tokens for each request
- Enforces scope-based access control
- Returns FHIR resources based on authorized permissions
3. Discovery Document (.well-known/smart-configuration)
- Published at
[fhir-base]/.well-known/smart-configuration - Contains URLs for authorization and token endpoints
- Lists supported scopes, capabilities, and features
- Enables dynamic client configuration without hardcoding URLs
// Example .well-known/smart-configuration response
{
"authorization_endpoint": "https://ehr.example.com/auth/authorize",
"token_endpoint": "https://ehr.example.com/auth/token",
"registration_endpoint": "https://ehr.example.com/auth/register",
"scopes_supported": ["launch", "patient/*.read", "user/*.read", "openid"],
"response_types_supported": ["code"],
"capabilities": ["launch-ehr", "launch-standalone", "client-public", "sso-openid-connect"]
}
Clinical Context
SMART on FHIR passes clinical context through the token response, enabling apps to know which patient, encounter, or user they're working with:
- patient - The FHIR ID of the patient in context (e.g., "Patient/123")
- encounter - The current clinical encounter, if applicable
- fhirUser - The FHIR resource representing the current user (Practitioner, Patient, RelatedPerson)
- need_patient_banner - Whether the app should display a patient context banner
- smart_style_url - URL to CSS for matching EHR styling
// Example token response with clinical context
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "launch patient/*.read openid fhirUser",
"patient": "123",
"encounter": "456",
"fhirUser": "Practitioner/789",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
App Registration and Client Types
SMART on FHIR supports different client types with different security characteristics:
Confidential Clients (server-side applications):
- Can securely store a client secret
- Authenticate with client_id and client_secret
- Preferred for server-to-server communication
- Can use refresh tokens
Public Clients (browser-based or mobile apps):
- Cannot securely store secrets
- Must use PKCE (Proof Key for Code Exchange) for security
- Authenticate with client_id only (no secret)
- More restricted token lifetimes
Registration Requirements:
- Client ID - Unique identifier for your application
- Redirect URIs - Whitelisted callback URLs
- Scopes - Requested permissions
- App Name and Description - For user consent screens
- Logo URL - Displayed during authorization
- Terms of Service/Privacy Policy URLs - Required by some EHRs
Refresh Tokens and Session Management
Access tokens have limited lifespans. Refresh tokens enable long-running sessions without requiring re-authentication:
// Using a refresh token to get a new access token
public String refreshAccessToken(String refreshToken) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "refresh_token");
params.add("refresh_token", refreshToken);
params.add("client_id", credentials.getClientId());
params.add("client_secret", credentials.getClientSecret()); // For confidential clients
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(
credentials.getTokenUrl(), request, Map.class);
Map<String, Object> tokenResponse = response.getBody();
return (String) tokenResponse.get("access_token");
}
Session Management Best Practices:
- Store refresh tokens securely (encrypted, not in browser storage)
- Implement token refresh before access token expires
- Handle refresh token expiration gracefully (re-authenticate user)
- Revoke tokens when user logs out
- Monitor for token theft indicators (unusual IP, location changes)
Backend Services Authorization
For system-to-system communication without user interaction, SMART defines a Backend Services flow using asymmetric keys:
- App generates a public/private key pair
- Public key is registered with the authorization server
- App creates a signed JWT assertion using the private key
- JWT is exchanged for an access token (no user consent required)
- Commonly used for batch processing, analytics, and data synchronization
Prerequisites
Before proceeding, ensure you have the following set up:
- Java Development Kit (JDK) installed and configured.
- Apache Maven installed and your project set up.
- Basic understanding of FHIR resources and RESTful APIs.
- Familiarity with OAuth 2.0 concepts.
- Access to a SMART on FHIR-enabled server for testing (we'll provide options in this tutorial).
- You can find all the code demonstrated in this tutorial on GitHub here
“Life is a journey, not a destination.” ~ Ralph Waldo Emerson
Step 1 of 5: Understanding SMART on FHIR
Before diving into the implementation, let's understand what SMART on FHIR is and how it addresses interoperability challenges in healthcare applications.
What is SMART on FHIR?
SMART on FHIR is a set of open specifications that integrates two key components:
- SMART: A framework that provides a standard way for applications to authenticate with EHR systems using OAuth 2.0.
- FHIR: A standard for exchanging healthcare information electronically, defining how healthcare information can be exchanged between different systems.
The combination of these two standards creates a platform where applications can be "substitutable" – meaning they can be added or replaced easily within an EHR ecosystem without requiring custom integration work.
Before SMART on FHIR, each EHR vendor had their own proprietary APIs and integration methods. This meant that developers had to create custom integrations for each EHR system, as illustrated below:
App 1 → Custom Integration → EHR Vendor A
App 2 → Custom Integration → EHR Vendor B
App 3 → Custom Integration → EHR Vendor C
SMART on FHIR addresses this issue by providing a standardized layer between applications and EHR systems:
App 1 ↘
App 2 → SMART on FHIR Layer → Any EHR with SMART on FHIR Support
App 3 ↗
This approach enables developers to create applications once and have them work with any SMART on FHIR-compatible EHR system, significantly reducing development and maintenance costs.
SMART on FHIR Authorization Flow
SMART on FHIR uses OAuth 2.0 for authorization, typically following these steps:
- The app requests authorization from the EHR's authorization server.
- The user authenticates and grants permission.
- The app receives an authorization code.
- The app exchanges this code for an access token.
- The app uses this token to access FHIR resources.
This process ensures that applications can only access the resources they have been authorized to use, maintaining security and privacy.
Step 2 of 5: Setting Up Your Java SMART on FHIR Project
Now, let's set up a Java project to implement a SMART on FHIR application. We'll use Maven to manage our dependencies.
Creating a Maven Project
First, create a new Maven project with the following structure:
smart-on-fhir-app/
├── pom.xml
└── src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── saravanansubramanian/
│ │ └── smartonfhir/
│ │ ├── App.java
│ │ ├── SmartClientCredentials.java
│ │ └── PatientViewer.java
│ └── resources/
│ └── static/
│ ├── index.html
│ └── css/
│ └── style.css
└── test/
└── java/
Next, configure your Maven dependencies in the pom.xml file:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.saravanansubramanian</groupId>
<artifactId>smart-on-fhir-app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hapi.fhir.version>6.6.0</hapi.fhir.version>
<spring.boot.version>2.7.0</spring.boot.version>
</properties>
<dependencies>
<!-- HAPI FHIR Client -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>
<version>${hapi.fhir.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r4</artifactId>
<version>${hapi.fhir.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- OAuth2 Client -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
This pom.xml includes the necessary dependencies for working with FHIR resources, implementing OAuth 2.0 authorization, and creating a simple web application using Spring Boot.
Creating Configuration Classes
First, let's create a class to store our SMART on FHIR client credentials:
package com.saravanansubramanian.smartonfhir;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "smart")
public class SmartClientCredentials {
private String clientId;
private String clientSecret;
private String fhirServerUrl;
private String authServerUrl;
private String tokenUrl;
private String redirectUri;
private String scope;
// Getters and setters
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public String getFhirServerUrl() {
return fhirServerUrl;
}
public void setFhirServerUrl(String fhirServerUrl) {
this.fhirServerUrl = fhirServerUrl;
}
public String getAuthServerUrl() {
return authServerUrl;
}
public void setAuthServerUrl(String authServerUrl) {
this.authServerUrl = authServerUrl;
}
public String getTokenUrl() {
return tokenUrl;
}
public void setTokenUrl(String tokenUrl) {
this.tokenUrl = tokenUrl;
}
public String getRedirectUri() {
return redirectUri;
}
public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
}
Next, create an application.properties file in the resources directory:
# Server configuration
server.port=8080
# SMART on FHIR configuration
smart.clientId=your_client_id
smart.clientSecret=your_client_secret
smart.fhirServerUrl=https://launch.smarthealthit.org/v/r4/fhir
smart.authServerUrl=https://launch.smarthealthit.org/v/r4/auth/authorize
smart.tokenUrl=https://launch.smarthealthit.org/v/r4/auth/token
smart.redirectUri=http://localhost:8080/callback
smart.scope=launch patient/*.read openid fhirUser
Replace the placeholder values with your actual SMART on FHIR client credentials. For testing purposes, you can use the SMART Health IT Sandbox, which provides a free testing environment for SMART on FHIR applications.
Step 3 of 5: Implementing the SMART Authorization Flow
Now, let's implement the SMART on FHIR authorization flow in our application. We'll create a Spring Boot application that handles the OAuth 2.0 authorization process.
Main Application Class
package com.saravanansubramanian.smartonfhir;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Controller for Authorization Flow
package com.saravanansubramanian.smartonfhir;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Patient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.UUID;
@Controller
public class SmartAuthController {
private static final Logger logger = LoggerFactory.getLogger(SmartAuthController.class);
private final SmartClientCredentials credentials;
private final FhirContext fhirContext;
private final RestTemplate restTemplate;
@Autowired
public SmartAuthController(SmartClientCredentials credentials) {
this.credentials = credentials;
this.fhirContext = FhirContext.forR4();
this.restTemplate = new RestTemplate();
}
@GetMapping("/")
public String home(HttpSession session) {
// Generate and store state parameter to prevent CSRF attacks
String state = UUID.randomUUID().toString();
session.setAttribute("oauth_state", state);
return "index";
}
@GetMapping("/launch")
public String launchApp(HttpSession session) {
String state = (String) session.getAttribute("oauth_state");
if (state == null) {
state = UUID.randomUUID().toString();
session.setAttribute("oauth_state", state);
}
// Build the authorization URL
String authUrl = credentials.getAuthServerUrl() +
"?response_type=code" +
"&client_id=" + credentials.getClientId() +
"&redirect_uri=" + credentials.getRedirectUri() +
"&scope=" + credentials.getScope() +
"&state=" + state +
"&aud=" + credentials.getFhirServerUrl();
logger.info("Redirecting to auth URL: {}", authUrl);
return "redirect:" + authUrl;
}
@GetMapping("/callback")
public String handleCallback(
@RequestParam("code") String code,
@RequestParam("state") String state,
HttpSession session,
Model model) {
// Verify state parameter to prevent CSRF attacks
String storedState = (String) session.getAttribute("oauth_state");
if (!state.equals(storedState)) {
logger.error("State parameter mismatch: expected {}, got {}", storedState, state);
return "error";
}
// Exchange authorization code for access token
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("grant_type", "authorization_code");
map.add("code", code);
map.add("client_id", credentials.getClientId());
map.add("client_secret", credentials.getClientSecret());
map.add("redirect_uri", credentials.getRedirectUri());
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(map, headers);
ResponseEntity<Map> responseEntity = restTemplate.postForEntity(
credentials.getTokenUrl(),
requestEntity,
Map.class
);
Map<String, Object> tokenResponse = responseEntity.getBody();
if (tokenResponse == null) {
logger.error("Failed to obtain access token");
return "error";
}
String accessToken = (String) tokenResponse.get("access_token");
String patientId = (String) tokenResponse.get("patient");
// Store token in session
session.setAttribute("access_token", accessToken);
session.setAttribute("patient_id", patientId);
return "redirect:/patient";
}
@GetMapping("/patient")
public String getPatientInfo(HttpSession session, Model model) {
String accessToken = (String) session.getAttribute("access_token");
String patientId = (String) session.getAttribute("patient_id");
if (accessToken == null || patientId == null) {
return "redirect:/launch";
}
// Create a FHIR client with bearer token authentication
IGenericClient client = fhirContext.newRestfulGenericClient(credentials.getFhirServerUrl());
client.registerInterceptor(new BearerTokenAuthInterceptor(accessToken));
// Retrieve patient information
Patient patient = client.read()
.resource(Patient.class)
.withId(patientId)
.execute();
model.addAttribute("patient", patient);
// Retrieve patient's medications
Bundle medicationRequests = client.search()
.forResource(MedicationRequest.class)
.where(MedicationRequest.PATIENT.hasId(patientId))
.returnBundle(Bundle.class)
.execute();
model.addAttribute("medications", medicationRequests);
// Retrieve patient's allergies
Bundle allergies = client.search()
.forResource(AllergyIntolerance.class)
.where(AllergyIntolerance.PATIENT.hasId(patientId))
.returnBundle(Bundle.class)
.execute();
model.addAttribute("allergies", allergies);
return "patient";
}
}
This controller handles the entire SMART on FHIR authorization flow:
- The home page displays a launch button.
- When the user clicks the launch button, they are redirected to the authorization server.
- After authentication and authorization, the user is redirected to the callback endpoint with an authorization code.
- The callback endpoint exchanges this code for an access token.
- The access token is then used to retrieve patient information and display it.
Creating Thymeleaf Templates
Now, let's create the Thymeleaf templates for our application. First, create a templates folder under src/main/resources and add the following files:
index.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>SMART on FHIR App</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>SMART on FHIR App</h1>
<p>Welcome to our SMART on FHIR application. Click the button below to launch the app and connect to your EHR.</p>
<a href="/launch" class="btn">Launch App</a>
</div>
</body>
</html>
patient.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Patient Information</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>Patient Information</h1>
<div class="patient-card">
<h2 th:text="${patient.nameFirstRep.nameAsSingleString}">Patient Name</h2>
<p>
<strong>ID:</strong> <span th:text="${patient.idElement.idPart}">ID</span><br/>
<strong>Gender:</strong> <span th:text="${patient.gender}">Gender</span><br/>
<strong>Birth Date:</strong> <span th:text="${patient.birthDate}">Birth Date</span><br/>
</p>
<div th:if="${patient.hasAddress()}">
<h3>Address</h3>
<p>
<span th:text="${patient.addressFirstRep.line}">Address Line</span><br/>
<span th:text="${patient.addressFirstRep.city + ', ' + patient.addressFirstRep.state + ' ' + patient.addressFirstRep.postalCode}">City, State ZIP</span>
</p>
</div>
<div th:if="${patient.hasTelecom()}">
<h3>Contact Information</h3>
<p th:each="telecom : ${patient.telecom}">
<strong th:text="${telecom.system}">System</strong>: <span th:text="${telecom.value}">Value</span>
</p>
</div>
</div>
<h2>Medications</h2>
<div th:if="${medications.hasEntry()}" class="resource-list">
<div th:each="entry : ${medications.entry}" class="resource-item">
<div th:with="med=${entry.resource}">
<h3 th:if="${med.hasMedicationCodeableConcept()}" th:text="${med.medicationCodeableConcept.coding[0].display}">Medication</h3>
<p th:if="${med.hasDosageInstruction()}" th:text="${med.dosageInstructionFirstRep.text}">Dosage</p>
<p th:if="${med.hasStatus()}" th:text="'Status: ' + ${med.status}">Status</p>
</div>
</div>
</div>
<div th:unless="${medications.hasEntry()}" class="no-data">
<p>No medications found.</p>
</div>
<h2>Allergies</h2>
<div th:if="${allergies.hasEntry()}" class="resource-list">
<div th:each="entry : ${allergies.entry}" class="resource-item">
<div th:with="allergy=${entry.resource}">
<h3 th:if="${allergy.hasCode()}" th:text="${allergy.code.coding[0].display}">Allergy</h3>
<p th:if="${allergy.hasClinicalStatus()}" th:text="'Status: ' + ${allergy.clinicalStatus.coding[0].code}">Status</p>
<div th:if="${allergy.hasReaction()}">
<p><strong>Reactions:</strong></p>
<ul>
<li th:each="reaction : ${allergy.reaction}" th:text="${reaction.manifestation[0].coding[0].display}">Reaction</li>
</ul>
</div>
</div>
</div>
</div>
<div th:unless="${allergies.hasEntry()}" class="no-data">
<p>No allergies found.</p>
</div>
</div>
</body>
</html>
error.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Error</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>Error</h1>
<p>An error occurred during the authorization process. Please try again.</p>
<a href="/" class="btn">Back to Home</a>
</div>
</body>
</html>
Also, let's add a simple CSS file to make our application look better:
style.css (in src/main/resources/static/css/):
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
background-color: #f5f7fa;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1, h2, h3 {
color: #2c3e50;
}
h1 {
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
margin-bottom: 20px;
}
.btn {
display: inline-block;
background-color: #3498db;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s;
font-weight: bold;
}
.btn:hover {
background-color: #2980b9;
}
.patient-card {
background-color: white;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.resource-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.resource-item {
background-color: white;
border-radius: 5px;
padding: 15px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.no-data {
background-color: #f8f9fa;
border-radius: 5px;
padding: 15px;
text-align: center;
color: #6c757d;
margin-bottom: 30px;
}
Step 4 of 5: Registering Your SMART on FHIR App
Before you can use your app with a SMART on FHIR-enabled EHR system, you need to register it with that system. The registration process typically involves providing the following information:
- App Name: A descriptive name for your application.
- App Type: Whether it's a confidential client (server-side) or public client (browser-based or mobile).
- Redirect URI: The URI where the authorization server will redirect after authentication.
- Scopes: The permissions your app requires (e.g., patient/*.read, user/*.* for provider-facing apps).
For testing purposes, you can use one of the following SMART on FHIR sandboxes:
- SMART Health IT Sandbox: A free, open sandbox for SMART on FHIR apps.
- Epic's FHIR Sandbox: Requires registration but provides a realistic Epic EHR environment.
- Cerner's FHIR Sandbox: Requires registration but offers a Cerner EHR environment.
For this tutorial, we'll use the SMART Health IT Sandbox, which doesn't require formal registration for testing.
Step 5 of 5: Running and Testing Your SMART on FHIR App
Now that we've built our SMART on FHIR application, let's run and test it:
- Build the application using Maven:
mvn clean package - Run the application:
java -jar target/smart-on-fhir-app-1.0-SNAPSHOT.jar - Open your browser and navigate to
http://localhost:8080. - Click the "Launch App" button to start the SMART on FHIR authorization flow.
- You will be redirected to the SMART Health IT Sandbox to select a patient and authorize the app.
- After authorization, you will be redirected back to your application, which will display the patient's information, medications, and allergies.
If everything is set up correctly, you should see a page displaying the selected patient's information, medications, and allergies.
Extending Your SMART on FHIR App
Once you have the basic SMART on FHIR app working, you can extend it in various ways:
- Add support for additional FHIR resources, such as Observation, Procedure, or Encounter.
- Implement charting or visualization for patient data using libraries like Chart.js or D3.js.
- Add features to write data back to the EHR using FHIR PUT and POST operations.
- Implement different launch scenarios, such as standalone launch or EHR launch.
- Add support for refreshing access tokens when they expire.
Here's an example of how you might extend your application to display a patient's vital signs:
@GetMapping("/vitals")
public String getPatientVitals(HttpSession session, Model model) {
String accessToken = (String) session.getAttribute("access_token");
String patientId = (String) session.getAttribute("patient_id");
if (accessToken == null || patientId == null) {
return "redirect:/launch";
}
// Create a FHIR client with bearer token authentication
IGenericClient client = fhirContext.newRestfulGenericClient(credentials.getFhirServerUrl());
client.registerInterceptor(new BearerTokenAuthInterceptor(accessToken));
// Retrieve patient information
Patient patient = client.read()
.resource(Patient.class)
.withId(patientId)
.execute();
model.addAttribute("patient", patient);
// Retrieve patient's vital signs
Bundle vitals = client.search()
.forResource("Observation")
.where(Observation.SUBJECT.hasId(patientId))
.and(Observation.CATEGORY.exactly().code("vital-signs"))
.sort().descending(Observation.DATE)
.count(10)
.returnBundle(Bundle.class)
.execute();
// Null-safe check before adding to model
int vitalsCount = (vitals != null && vitals.hasEntry()) ? vitals.getEntry().size() : 0;
model.addAttribute("vitals", vitals);
model.addAttribute("vitalsCount", vitalsCount);
return "vitals";
}
This method retrieves a patient's vital signs and passes them to a "vitals" view for display. You would then create a corresponding Thymeleaf template to display this data.
Conclusion
In this tutorial, we've learned how to build a SMART on FHIR application using Java and Spring Boot. We've implemented the SMART authorization flow, retrieved patient data using the FHIR API, and displayed it in a user-friendly interface.
SMART on FHIR provides a powerful framework for building healthcare applications that can integrate with various EHR systems without requiring custom integration code for each system. This interoperability is essential for creating a vibrant ecosystem of healthcare applications that can improve patient care and streamline clinical workflows.
By mastering SMART on FHIR, you can develop applications that can be easily adopted by healthcare organizations using different EHR systems, significantly expanding your potential user base and impact on healthcare.
By mastering SMART on FHIR, you can develop applications that can be easily adopted by healthcare organizations using different EHR systems, significantly expanding your potential user base and impact on healthcare. In the next article in this series, we will explore FHIR profiles and demonstrate their practical application using Canadian Core profiles as an example.