Building a SMART on FHIR web application

In our previous post we got into the basics of FHIR, what it is, who it’s made for, and why it is useful. We then made a few queries to sort of get to know the system. In this section we’re going to take that little example to the next level by creating a client application that will consume the medical record and data and present it in a user interface within a web browser.

Authentication

If you’ve worked with APIs in the real world you likely noted something missing from our first few examples. The HAPI server is an open development server, but in the real world we only want to be passing our information from FHIR to applications that we trust. Thankfully for us web developers, the standardized authentication mechanisms designed around connecting to FHIR are built on using technology most web developers are already familiar with, OAuth2.

That’s right, under the hood, SMART authentication is really just a healthcare specific name for doing the type of standardized authentication that is ubiquitous across the internet. Once again, the standards are designed to encourage non-healthcare experienced developers to quickly and easily start building applications in against the EMR. There are plenty of great resources online if you would like to learn more about the OAuth2 security protocol. A basic understanding of it will help as we dig into our next examples.

Application Registration in the EMR

Before we can leverage the SMART authentication built into FHIR certain information about our application needs to be registered within the EMR system.  No matter how an app registers with an EHR’s authorization service, at registration time every SMART app SHALL:

  • Register zero or more fixed, fully-specified launch URL with the EHR’s authorization server
    • This launch URL is the url of the webpage that will be displayed when the user launches the SMART application from within the EMR.
  • Register one or more fixed, fully-specified redirect_uris with the EHR’s authorization server. Note that in the case of native clients following the OAuth 2.0 for Native Apps specification (RFC 8252), it may be appropriate to leave the port as a dynamic variable in an otherwise fixed redirect URI.
    • After the user has completed providing their credentials to the EMR system, this URL is the one that the system will redirect the user to on our web app.
    • Appended to this url will be the critical information our application needs to be able to send authorized requests to the FHIR server.

FHIR Javascript Library

Before we dig into an example of a launch.html page and a landing page for post authentication let’s take a quick look at an open source javascript library that can really save us a lot of time when developing SMART applications.

The Launch Page

As mentioned above, the first page we need to add to our application in order for the SMART authentication process to work is a launch page. Below is an example of a very simplified launch page. 

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>Launch My APP</title>
        <script src="https://cdn.jsdelivr.net/npm/fhirclient/build/fhir-client.js"></script>
    </head>
    <body>
        <script>
            FHIR.oauth2.authorize({

              // The client_id that you should have obtained after registering a client at
              // the EHR.
              clientId: "my_web_app",

              // The scopes that you request from the EHR. In this case we want to:
              // launch            - Get the launch context
              // openid & fhirUser - Get the current user
              // patient/*.read    - Read patient data
              scope: "launch openid fhirUser patient/*.read",

              // Typically, if your redirectUri points to the root of the current directory
              // (where the launchUri is), you can omit this option because the default value is
              // ".". However, some servers do not support directory indexes so "." and "./"
              // will not automatically map to the "index.html" file in that directory.
              redirectUri: "index.html"
            });
        </script>
    </body>
</html>

Notice that in the head of the page we’re importing the fhir-client.js file from a CDN. Then inside the body of the document we’re calling some functions from the FHIR client library. This call to FHIR.oauth2.authorize() has some parameters that deserve a closer look.

clientIdWhen you register your application in the EMR, you will be provided with a clientId that goes here. This tells the EMR which application is trying to connect.
scopeThese scopes are used to control what level of access the application has to the EMR. In this case we’re asking for the following privileges:
LaunchGet context information about the launch from the EMRfhirUserGet information about the current logged in user from thepatient/*.readRead information about any patient
redirectUriThe route to the page that the user should land on after authentication is completed.

The Landing Page

The other piece of the puzzle our application is responsible for is providing a landing page where the EMR will send the user after a successful login. Along with this being the first page of the application the user will see after they launch your app, this page is also responsible for grabbing the necessary information from the url parameters that we’ll need to make authenticated request to the FHIR server from javascript running in the users browser.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Example SMART App</title>
        <script src="https://cdn.jsdelivr.net/npm/fhirclient/build/fhir-client.js"></script>
        <style>
            #patient, #meds {
                font-family: Monaco, monospace;
                white-space: pre;
                font-size: 13px;
                height: 30vh;
                overflow: scroll;
                border: 1px solid #CCC;
            }
        </style>
    </head>
    <body>
        <h4>Current Patient</h4>
        <div id="patient">Loading...</div>
        <br/>
        <h4>Medications</h4>
        <div id="meds">Loading...</div>
        <script type="text/javascript">
            FHIR.oauth2.ready().then(function(client) {
               
                // Render the current patient (or any error)
                client.patient.read().then(
                    function(pt) {
                        document.getElementById("patient").innerText = JSON.stringify(pt, null, 4);
                    },
                    function(error) {
                        document.getElementById("patient").innerText = error.stack;
                    }
                );
               
                // Get MedicationRequests for the selected patient
                client.request("/MedicationRequest?patient=" + client.patient.id, {
                    resolveReferences: [ "medicationReference" ],
                    graph: true
                })
               
                // Reject if no MedicationRequests are found
                .then(function(data) {
                    if (!data.entry || !data.entry.length) {
                        throw new Error("No medications found for the selected patient");
                    }
                    return data.entry;
                })
               

                // Render the current patient's medications (or any error)
                .then(
                    function(meds) {
                        document.getElementById("meds").innerText = JSON.stringify(meds, null, 4);
                    },
                    function(error) {
                        document.getElementById("meds").innerText = error.stack;
                    }
                );

            }).catch(console.error);
        </script>
    </body>
</html>

Analyzing what is happening in this code, we can see that here again, the javascript library has essentially abstracted away all of the heavy lifting that needs to be done for authentication. The work being done inside FHIR.oauth2.ready function is grabbing the authentication information from the redirect url, holding it in memory, and will automatically apply it to any outbound FHIR request made via the library after that.

You can see that once the library returns a client that is authenticated and ready we are immediately pulling information from the EMR. First client.patient.read() will pull down the patient resource profile for the selected patient. Most of the time when applications are working in FHIR they are launched within the context of a given patient. Calling this function ensures that we know the scope of which patient we’re looking at.

At the same time that we’re pulling the current patient information, we’re also making a second request. We’re going to pull all the MedicationRequests that this patient has in the EMR system. Compare this query for getting all the medication requests with the GET url we looked at earlier in this book. You can see that the javascript library is doing some simplification for us. It is keeping track of the FHIR server URL, so we need only to pass the path to the specific resources we’re looking for. You can also see that the client.request function takes in an object of parameters that can affect how the data is returned from FHIR.

In this example it is telling the FHIR server to blow out any references to a medication out into the full profile. Had this parameter not been included, the response would only return an id for the medication in the request profile. We would have to go back to the FHIR server and look up this medication by ID to get the same information. Dereferencing profiles is a great way to lower the number of calls that need to be made against the FHIR server, which can greatly increase the performance of your SMART applications.

Lab: A Simple (and Ugly) SMART App

We now have the two basic building blocks we need in place to start writing real web applications that talk to an EMR system. First, we have the knowledge of how to request information by speaking to the EMR. We do this using HTTP verbs and json objects in the shape of the resources defined in FHIR. We also have a method for registering the application, and authenticating it against the medical record system using SMART. Let’s take these technologies for a little test drive.

SMART Testing Harness

Although a SMART application looks a lot like any other html web page, if you try to test your SMART application by directly opening the index.html file or the launch.html file it’s going to look a lot like the application is broken. In order for a SMART app to work, the launch.html page has to be provided with some special url parameters that our application can use to start the authentication process.

Luckily for us there are some wonderful tools we can use to fake an EMR launch. One such tool is the SMART Launcher maintained by SmartHealthIT.org. 

https://launch.smarthealthit.org/

On this page there’s a lot of information you can configure about the context with which your SMART application can be launched. As you change settings on this page, behind the scenes it is adding and removing parameters to your launch URL. If I right click on the launch button and copy the address it looks like this:

https://jakec77.github.io/FHIR-Benders-SMART-lab-1/launch.html?iss=https%3A%2F%2Flaunch.smarthealthit.org%2Fv%2Fr4%2Ffhir&launch=WzAsIjg3YTMzOWQwLThjYWUtNDE4ZS04OWM3LTg2NTFlNmFhYjNjNiIsIiIsIkFVVE8iLDAsMCwwLCIiLCIiLCIiLCIiLCIiLCIiLCIiLDAsMV0

This URL is calling the hosted version of my launch.html file that is being on Github pages. Everything after the question mark is information that the javascript on our launch.html page will use to begin setting up the secure connection to the FHIR server. In this case the parameter “iss” is information about where the FHIR server is that we want to authenticate against. If I use this URL to navigate to my page and everything is set up correctly, I should get rerouted to the login page of the EMR.

This is the login screen for the SMART Health test EMR. Had this been a launch from Cerner or Epic, the app would behave exactly the same, the only difference would be the values in the “iss” parameter. This is what allows a single SMART app to work against any SMART compliant EMR. If we look at the actual URL for the EMR that we were taken to, we can see that once again a whole bunch of extra information has been tacked on to the URL.

https://launch.smarthealthit.org/provider-login?client_id=2fb2893b-cfa6-4bee-82ab-9363cbbc036b&response_type=code&scope=user%2FPatient.read+user%2FEncounter.read+user%2FObservation.read+launch+online_access+openid+profile&redirect_uri=https%3A%2F%2Fjakec77.github.io%2FFHIR-Benders-SMART-lab-1%2F&state=12345&aud=https%3A%2F%2Flaunch.smarthealthit.org%2Fv%2Fr4%2Ffhir&launch=WzAsIjg3YTMzOWQwLThjYWUtNDE4ZS04OWM3LTg2NTFlNmFhYjNjNiIsIiIsIkFVVE8iLDAsMCwwLCIiLCIiLCIiLCIiLCIiLCIiLCIiLDAsMV0&login_type=provider

A lot of the parameters in this url are driven by information we configured for the FHIR connection in the javascript on the launch.html page. The scope, state, and redirectUri are all represented The resulting page displayed will be where the user provides their credentials to the EMR. The SMART Health IT organization server has some robust datasets for patients with different medical conditions. Here’s our initialization of the FHIR connection on the launch page for reference.

      FHIR.oauth2.authorize({
        'client_id': '2fb2893b-cfa6-4bee-82ab-9363cbbc036b',
        'state': '12345',
        'scope':  'user/Patient.read user/Encounter.read user/Observation.read launch online_access openid profile'
      });

At this point, we can click the login button. In a production setting this is the point where the EMR would validate the application is correctly accessing medical information. If the clientId was wrong, we asked for a scope we haven’t been granted permission for, or provided a redirectUri that doesn’t match what has been registered in the EMR the connection will fail. If all of that information is good, or you’re on a test server that is wide open like this one, then after you login you will be sent to the url specified in the redirectUrl parameter, or index.html if no redirect parameter was provided.

Finally we have reached our index.html page with all the necessary information needed to get down to the business of doing something interesting with medical information. You can tell whether or not the application is working as there should be patient information showing on the page if it could be pulled from the FHIR server.

Adding Observations

If you have a patient name showing up after running through the login process, you’ve got yourself a working connection to a FHIR server! Now we can start to build a good old fashioned web application the way we would if we were consuming data from any other backend web service. To take this for a test drive, let’s query and display some observation information about the selected patient. Let’s take a look at what a raw GET url would look like for this.

`http://fhir.org/observations?patient=${patientId}`;

You can see we’re querying the observations FHIR resources and filtering them based on a patient parameter. Once again, by leveraging the built in tool available as part of the javascript FHIR library, we can leverage the existing SMART authentication to make this query work. Here’s what that looks like in the javascript

    var observations = smart.patient.api.fetchAll({
                    type: 'Observation',
                  });

Here we’re using the existing smart object that was instantiated and authenticated, and fetching information specific to the patient context. In this case, we’re going to be getting all the patient’s observation records. Let’s continue and add a quick javascript function that will read observations and add them to an html table for display.

  function populateObservationTable(obs){
    $('#obsTable').empty();
    $('#obsTable').append("<tr><th>Text</th><th>Value</th><th>Unit</th>");

    for(var i in obs){
      var ob = obs[i]
      if(ob.valueQuantity){
        var row = "<tr><td>" + ob.code.text + "</td><td>" + ob.valueQuantity.value + "</td><td>" + ob.valueQuantity.unit + "</td></tr>";
        $('#obsTable').append(row);
      }
    }
  }

This is a pretty simple function. We pass in our list of observations, and then add a header and rows to the table for each of them. We’re displaying the code.text field which is typically a readable description of what this observation actually was. If the observation also has values associated with it, we display the corresponding value and unit. One last piece of code remains to be added, the code that will wait for the observation query to return and pass that list into our populateObservationTable function. Here’s what those additions might look like.

$.when(pt, obv).done(function(patient, obv) {
    var gender = patient.gender;
    var fname = '';
    var lname = '';

    if (typeof patient.name[0] !== 'undefined') {
      fname = patient.name[0].given.join(' ');
      lname = patient.name[0].family;
    }

 
    var p = defaultPatient();
    p.birthdate = patient.birthDate;
    p.gender = gender;
    p.fname = fname;
    p.lname = lname;
    populatObservationTable(obv);
    ret.resolve(p);
  });

Now if we start things up and run them, we’ll see that we’ve got a whole lot more information now showing up in our SMART application. Note that the results may vary depending on which patient context you’re opening the application in from the SMART app launcher. If you don’t see any major errors, and your table is empty, try a few other patients. In my testing most of them have at least a few observation records.

The EMR Is Your Oyster

I know it’s not much fun to look at (I’m mainly a backend guy), but I think this SMART app example gives you a solid idea of how the rubber meets the road when it comes to connecting to and interacting with the electronic medical record system through FHIR. Consider this first app you built a little sandbox, from here you can dig into what is available in the FHIR specification and start building an application that might have some use to you. I will take this moment to remind you that the functionality available to you will vary greatly between environments. For example, builds of Epic and Cerner don’t meet all the specifications laid out in FHIR. Even once your SMART application is complete and ready for the market, you’ll still want to do some testing per environment to ensure all the FHIR goodness you’re using is actually available in that system.

About the Author

Leave a Reply

Your email address will not be published. Required fields are marked *

You may also like these