Since our initial FHIR implementation, our need for additional FHIR support has proliferated — in order to unlock things like FHIR data quality testing and connecting to the Health Information Exchange (HIE), we needed more elements of a working FHIR server, especially since several desirable external systems like these utilize FHIR APIs to communicate. More importantly, as our team and codebase scales, we’ve been experiencing an increasing need to evolve our FHIR implementation past the initial infrastructure we initially implemented. In particular:
- Our FHIR implementation was incomplete — intentionally so as to urgently unblock development, but still incomplete nonetheless. We were missing most of the operations that a FHIR server typically supports, instead relying heavily on exactly three endpoints for all our FHIR-related interactions. We wanted the capability to both validate and extend our FHIR implementation without having to build every piece from scratch ourselves.
- Python FHIR support is limited — we were relying on the SMART on FHIR python client-py library for parsing FHIR resources to and from the database, but there is no complete FHIR implementation library for python. Combined with some issues with getting the library on R4, it became clear that we needed a solution that was easy to maintain as the FHIR standard and our use cases evolved. For instance, earlier this year I shipped a feature that integrated historical versioning for FHIR resources, which was a new use of FHIR that I had to build from scratch.
- FHIR had a large learning curve — our scrappy implementation left some tribal knowledge and a good bit of crufty code that made learning FHIR a taller task than it needed to be. Having a dedicated FHIR server forces a decoupling (a la microservices) of FHIR from our backend services, meaning not only will development be well-defined (by strictly interacting with FHIR through an API), but we will have dedicated engineering resources toward FHIR infrastructure and teamwide FHIR literacy. Finally, decoupling should allow us to integrate with external FHIR data sources down the road.
It was a relatively short search. In addition to the considerations from above, we really valued:
- Ease of implementation and upkeep, since we are a small engineering team
- Object oriented language, preferably Python or Java
- Minimize (preferably eliminate) schema changes
- In-house data hosting
- Thorough documentation, active online community, and actively developed for the foreseeable future
HAPI FHIR checked all the boxes for us (particularly the Plain Server implementation). In short, the plain server provides a structure by which the developer can implement the FHIR REST operations on top of a pre-existing schema, and HAPI FHIR will handle wiring up the routes. In our case, we had an existing FHIR schema (detailed in a previous blog post), and we could build out the Java entities and HAPI FHIR infrastructure on top of that to get to a functional FHIR server. HAPI FHIR was perfect for us except that…
Our teamwide Java expertise was low, given that we had been developing our backend purely in Python since Curai’s inception. Compounding things, I could not find a single end-to-end, minimal HAPI FHIR implementation tutorial, which made it especially tough to get started. After a few weeks of Googling, coding, and cryptic errors, I was able to piece together several different tutorials to make a minimal example of a HAPI FHIR plain server. Obviously, our production implementation has a lot more bells and whistles, but these will vary from use case to use case so I think it’s most helpful to see a minimal example that you can eventually build on top of. When building your own server, three resources that I found particularly helpful are the FHIR community board, HL7 Confluence page, and DevDays presentations from years prior.
Before we jump into the code, below is a diagram of how each part of our FHIR infrastructure exists in our backend right now (simplified for the Patient FHIR resource type). The “Python” box is what our current FHIR systems operate on as described by Viggy in his blog post.The “Java” box is what we’re implementing in this tutorial. As shown, both are simultaneous consumers of the postgres table
fhir_patient, at least until we migrate fully to HAPI FHIR (which will fully deprecate the “Python” box). Let’s get started with how to implement the “Java” box! Step-by-step instructions are below and the full code is here!
Author’s note: for each step, please click on the (Code) link in the title to view the code changes required for each step before running the commands specified in the description!
1. Setting up spring boot (Code)
The first step to setting up our FHIR server is to set up a base Java webapp. To do this, head to Spring Initializr and add “Spring Data JPA”, “Spring Web”, and “H2 Database” (the link provided should have all of them already). Generate and download the bundle, and you have a base web server that can connect to a real H2 database. Later we will connect it to the real Postgres database.
2. Adding in HAPI (Code)
Now we’ll configure our Spring Boot server to work with HAPI FHIR’s server infrastructure. Note that HAPI FHIR’s REST API is configured differently than the typical Java Spring REST API routes, which is why we need to do this step.
Open up the project in IntelliJ (or your favorite Java IDE) and apply the code changes in the section title.
./mvnw spring-boot:run from the project’s directory will pop up a server, and you should be able to download the server’s (empty) capabilities at
localhost:8080/metadata. Right now, it should indicate that there are no actual capabilities supported because we haven’t implemented anything yet! If you’re getting an error, make sure you have a JDK installed!
Passing around the HAPI FHIR Context (Code)
The HAPI FHIR Context is what we use to parse strings and bytes into Java objects (and back). It’s the core of HAPI FHIR, and we’ll need to use it in every piece of code that relates to HAPI FHIR. Since it is expensive to create, we want to create it just once, and access it from everywhere it is needed. Hence static methods!
3. Creating a Patient Provider (Code)
Let’s make our FHIR server do something! In order to create an endpoint for a resource, we need to implement a provider for the resource. Then, we need to create a method and annotate it with the appropriate HAPI FHIR decorator. This will automatically configure the server to call that method when the route corresponding to the method is hit via the API.
Even though we’re not connected to a real database yet, we can still return some fake data to test our HAPI FHIR server. In this case, we’ll create a fake Patient and implement the read endpoint so we can view the patient.
In this case, the
@READ annotation configured a
GET operation to the Patient endpoint by id, which is accessible at
localhost:8080/Patient/1 (sub in any id — we just need a dummy one to trigger the read endpoint) and you should see an XML file with Lord Farquaad’s Patient data.
After applying the code, going back to
localhost:8080/metadata, we should now see that the server supports
Read for Patients. HAPI FHIR configured this automatically, and all future providers and methods will also be noted in this
4. Connecting Patient to the database
Adding in Postgres (Code)
Now that we know how to interact with the HAPI FHIR to get some dummy data, we can connect the API to an actual Postgres Database. You’ll need to create a local Postgres database with a username and password to supply to the
Supplying Patient Provider with actual data (Code)
As noted in Viggy’s FHIR post, our FHIR data follows a schema where the entire resource is stored as a
jsonb column. Create a
fhir_patient table in the database with a
jsonb column titled
resource (and an
id primary key column) and seed the table with a fake Patient JSON.
We need to add in support for
jsonb columns, since Hibernate (the Java ORM) does not support it natively — to do this, we’ll use a library called Hibernate-Types!
After applying this diff, when querying for the Patient resource at
localhost:8080/Patient/1, you should get a serialization error. Why? The Java object serialized by Jackson (the default serializer for Hibernate) cannot be cast into a HAPI FHIR Patient instance. We need to configure the serializer to use the HAPI FHIR parser instead. Good thing we can use the HAPI FHIR context that we configured in step 2!
Adding in custom serialization and deserialization (Code)
With this, we tell Hibernate + Jackson to serialize/deserialize Patient types with the custom ObjectMapper that we provide. Going back to
localhost:8080/Patient/1 should result in the exact JSON that you seeded the table with!