FHIR Client and Server

Introduction

This FHIR server-client example demonstrates how to use the FHIR API to access health care information. The two channels FHIR Server and FHIR Client work together to demonstrate how to access and manipulate data using a FHIR based API.

This purpose of the FHIR server channel is to provide an example web service that allows for health care information to be accessed and manipulated using a FHIR based API. It supports a basic set of FHIR operations like read, search, update, delete, and create. These operations support both XML and JSON data formats.

The workflow the server demonstrates reflects a simple FHIR setup that can act as a starting point for a number of different FHIR projects. This setup has the FHIR web server receiving, interpreting and validating requests, then reading and writing to an included database that contains a patient table. It was built in such a way that it can be adapted to support more complex database setups, or even integrating with something different altogether such as HL7.

The FHIR client channel demonstrates the types of operations that are supported on the FHIR Server. It can be used to visually see the different types of requests that are can be made and the format that they need to be in. It can also serve as a tool to trace server responses, or create new requests to send that can then be used as sample data within the server.

If you have any questions please contact us at support@interfaceware.com.

Note: Real world applications of FHIR are going to be more complex than this example.

The FHIR server is a proof-of-concept or foundation for building your own FHIR projects.

Please feel free to contact us at support@interfaceware.com to discuss you FHIR project needs!

Using the Channels [top]

  • Import the FHIR Server and FHIR Client channels from the Iguana Webservices repository
  • Make sure the FHIR Server channel is on and open the FHIR Client’s translator script
  • Experiment with the code to find out how it worksAdapt the code to your own requirements
  • Interactive scripting help is included for this module
  • To execute the code you will need to the RunTests variable at the start of main() to true:
  • The code uses automatically uses your Iguana server address when generating request URLs, as buildRequestUrlBeginning() uses the builtin iguana.webInfo() API function to retrieve information:

  • Each FHIR operation creates URL requests for XML and a JSON, this is what the Read requests look like:
  • You can inspect the FHIR results by clicking on the trace results in the annotation window, this is what the Read results look like:
  • If you have altered the example database while experimenting and would like to reset it to it’s original state, simply delete FHIR_PatientData.sqlite in the Iguana working directory (on Windows something like C:\Program Files\iNTERFACEWARE\Iguana). The FHIR Server (channel) will create a fresh copy on the next request (copied from your shared modules folder)
    Note: On Windows you will need to stop the FHIR Server channel before you can delete the database file (Windows prevents you from deleting a file if it is currently in use).
  • Adapt the code to your own requirements: Use these examples as a basis for your own requests which you can run in the FHIR client, or from another channel

This is the github code for the main module in the FHIR Server channel:

This is the github code for the main module in the FHIR Client channel:

How the FHIR Client works [top]

The FHIR Client is broken into multiple code sections, with each one demonstrating a FHIR operation:

  • READ: The example database the FHIR Server is communicating with is pre-populated with 2 entries, so querying for id 1 or 2 will return a valid result when experimenting with this type of operation. This is observed within the code for this section, where a patient resource with id=1 is queried using a GET request in both JSON and XML formats.
  • UPDATE: takes the result from the READ operation above it and flips the active parameter to the inverse value (as an example of a field that may be updated in a real world scenario). It will then make a PUT request to the FHIR Server to update that patient in the database to have this new value.
  • SEARCH: in this section another GET request is made. However, unlike the READ operation, there is no need to specify an id in the url. When no id is specified, a search is performed on all entries of that resource type. These are returned in what is called a resource bundle, which is a FHIR resource itself and is basically just a collection of items of the requested type.
  • CREATE: not surprisingly this section creates a new resource on the server. This section uses the accompanying file FhirPatient.lua in order to do this. Inside you will find two variables containing JSON and XML strings representing two new FHIR patient resources (with all the relevant data fields that this example server supports, patient resources can contain much more data if needed.) In this example, the client accesses the JSON patient string and sends a POST request to the FHIR Server. This successfully creates a new patient as seen by the 200 response we get back from the server. As per FHIR standard, the id of the newly created resource entry can be found within the response “location” header.
  • DELETE:  the code here takes the location of the newly created resource from the previous section, and sends a DELETE request to the server. This will remove it from the database.

Tip: For more information on the FHIR operations that this FHIR server supports as well as all the other defined operations within the FHIR standard, refer to the FHIR REST api documentation

How the FHIR Server works [top]

The FHIR Server code structure was designed to be very modular to make it easy to navigate and understand what the different pieces are doing. How the code works is really only important if you intend on Adapting the FHIR Server.

Processing and Routing Requests:

  • If you are familiar with FROM HTTPS components in Iguana, you will know that all request data enters through the main() function in main.lua.
  • The request is parsed and the function checks the type of HTTP request that was made (GET/PUT/POST/DELETE).
  • Depending on the request type, it will get routed through one of four applicable handlers. Each one of these handler functions can be found in their own file within web/handlers/types/ and are joined into one controller object in web/handlers/controller.lua.
    • This controller file will return the object containing all the handler functions from each handler file when included with a require statement. This is done so that we can access each function through the “Handlers” object. (instead of having to require each handler file individually).
      local Handlers  = require 'web.handlers.controller'
    • This is a common pattern within the FHIR server code in order to keep resources organized. You will see it more than once, so it is important that you understand how it works and why it was used. (especially if you intend on adapting the code.)
  • Within each one of these handler functions is the logic that validates the request URL to make sure it follows the format outlined in the FHIR standard.  Most of this validation logic can be found in web/utils.lua (along with other web helpers). For reference, each handler function contains comments outlining what format a valid request takes. After the URL has been validated it will perform logic specific to that handler type…

Request Handler Functions:

The following bullets outline each request handler function defined in web/handlers/types/

  • GET :
    • In this FHIR Server, the GET request is the only request that handles two different FHIR operations: READ and SEARCH.
    • It is a READ if the request specifies an id in the form of https://example.com/path/{resourceType}/{id}. This will read from our database a FHIR resource with the type specified, and the unique id specified.
      • If it is determined that it is a READ, then the handler calls into the database (see Database Access below) to retrieve the entry.
      • Once the entry is retrieved and stored into a database result set variable (assuming no errors), the result set is mapped into a FHIR object in either XML or JSON (see FHIR Mappings and FHIR Resource Profiles and Objects) and returned to the browser in the body of a 200 response.
    • It is a SEARCH operation if only a resource type is specified (with no id): https://example.com/path/{resourceType}/. This will read from our database all FHIR resource entities with the type specified.
      • If it is determined that it is a SEARCH, then the handler calls into the database file (see Database Access below) to retrieve all the entries for that type.
      • Once the entries are retrieved and stored into a database result set variable (assuming no errors), the result set is mapped into multiple FHIR objects in either XML or JSON (see FHIR Mappings and FHIR Resource Profiles and Objects), with one object per found entity.
      • The SEARCH operation then needs to package up all results into what is called a FHIR bundle, which is itself a FHIR resource. You will see this towards the bottom of the function, with comments explaining further. After they are put into the bundle, the result is returned to the browser in the body of a 200 response.
  • POST :
    • The POST handler function handles the FHIR CREATE operations. The url takes the form of https://example.com/path/{resourceType}/, with the FHIR resource that is intended to be created contained in the request body.
    • After validating the URL, the request body is extracted and validated as well to make sure it is valid XML or JSON.
    • We then perform a mapping from the posted FHIR resource to a database table. (see FHIR Mappings)
    • A new resource entry is then created using the mapped database table. The server will return a 201 response to indicate a successful creation, with the location of the new resource on the server being inserted into the location header in the response. (as per FHIR standard)
  • PUT :
    • The PUT handler function handles the FHIR UPDATE operation. The url takes the form of https://example.com/path/{resourceType}/{id}, with the id being the id of the resource entity we are intending to update (with the FHIR resource information contained in the request body).
    • After validating the URL, the request body is extracted, parsed, and validated to make sure it is valid XML or JSON.
    • A mapping in then performed from this posted FHIR resource object to a database table. (see FHIR Mappings)
    • The database is then checked to see whether the resource with the specified id exists already. If it doesn’t then this operation will just create a new one with the newly mapped database table (as per the FHIR standard). If it does exist, it will use the database table to update the existing entry.
    • If the resource existed and an database update was performed, it returns a 200 response. If it didn’t exist and a brand new entry was created, it returns a 201 response identical of that to the FHIR CREATE operation described above.
  • DELETE :
    • The DELETE handler function handles FHIR DELETE operations. The url takes the form of https://example.com/path/{resourceType}/{id}, with the id being that of the resource entity we are intending to delete.
    • After url validation, the handler attempts to delete the entry from the database with the specified id. The server will then return a 204 response indicating (as per the FHIR standard) that the entity was successfully deleted or didn’t exist at all.

Server Responses (Success and Error):

  • The code in the server has very detailed comments describing each scenario in which a success response or error response is returned at the point in which it happens. The response functions are defined in web/responses.lua.
  • Errors:
    • 400 error responses are usually returned when a request is performed with a improperly formatted URL (i.e you included an id in the URL for a CREATE operation, which isn’t allowed as per FHIR standard), or the FHIR resource body being PUT/POSTED is not valid JSON or XML.
    • 500 error responses usually indicate a database error or some other internal server error dealing with resource mapping.
    • This server implements very basic error handling using basic FHIR “Operation Outcomes”. More comprehensive error responses should be researched and implemented if intending to follow the FHIR standard more closely and implement this in a production environment.  For more information see https://www.hl7.org/fhir/operationoutcome.html.
  • Successes:

Database Access:

  • All database connection and access logic is self contained within database.lua. It is structured similarly to other components in this server, in that requiring the file will return an object with all the database access functions defined on it.
  •  As mentioned in the FHIR Client section, when the channel starts/takes a request in, it will check for a copy of the FHIR_PatientData.sqlite test database in Iguana’s working directory to work off of. If it is not present (like will be the case for the first time starting the channel) it will copy over a fresh copy from other/FHIR_PatientData.sqlite (the logic for this is seen at the top of the file). This is done so that, while experimenting with the server, if you ever want to start with a fresh test database you can just delete the copy in the Iguana working directory to start working with a fresh data set.
  • The server opens up a database connection only once when the channel starts, and will keep this connection open as long as the channel is running.

FHIR Resource Profiles and Objects:

  • When mapping data to/from the database and FHIR resource objects, a FHIR resource profile string is required to create the object within the translator.
  • Each type of resource profile the server handles is defined in fhir/resources/definitions/types/ with a file name corresponding to the resource name. The resources are summarized and joined in fhir/resources/definitions/controller.lua (similar to what was done above)
  • In each resource file, there is an object that stores the resource profile strings in both JSON and XML. These strings can be retrieved from www.hl7.org/fhir. (For example the JSON string for a Patient can be found at https://www.hl7.org/fhir/patient.profile.json)
  • FHIR resource objects can be programmatically created by having Lua parse and read these resource profile strings and then create XML tables or JSON objects pre-populated with fields as defined within the profile. These objects are created in functions defined in fhir/resources/objects.lua as seen below (where we are creating an empty Patient resource) …
    FHIR Object Creation Functions
  • FHIR resource field values can take on a variety of different datatypes. These datatypes each fall into one of two categories: Primitive or Complex. The datatypes are defined and explained in detail at https://www.hl7.org/fhir/datatypes-definitions.html.
    • When a resource profile string describes a field as having one of the complex datatype’s, that complex datatype’s string (in either JSON or XML format) is retrieved from fhir/complex_datatypes.lua, parsed, then inserted into the FHIR object that is being created.
    • The definitions of these datatypes obtained from https://www.hl7.org/fhir/datatypes-definitions.html are not valid XML/JSON, so the original copied strings reside in fhir/complex_datatypes_reference.lua, which is purely for reference purposes as it contains better descriptions of each datatype that you can refer to. The cleaned up and parseable versions the server uses to build FHIR objects are in fhir/complex_datatypes.lua.

FHIR Mappings:

  • When a request handler wants to map a FHIR resource to a database object or vice versa, functions defined within fhir/mappings/utilities.lua are called.
  • An example of this is seen below when calling MappingUtils.getFhirFromDbResults(), which is called when a READ request has been made to the server for a patient resource (so the function call has been made from the request handler in web/handlers/types/get.lua)…
    Functions in the FHIR Server for mapping from FHIR objects to DB objects or vice versa.
  • In this case, we have passed in a retrieved Database result set (DbResult) and want to map this data to a FHIR object to return to the browser. The format of the resultant resource (XML/JSON) is passed in, as well as the resource name (in this case ‘patient’)
  • We calculate a ‘mapping type’ string (in this case “DB2json”) using the data format which we will use in order to retrieve the appropriate mapping function needed.
  • If you look at the sidebar in the picture, you will see the following four folders:
    • fhir/mappings/DB2json/
    • fhir/mappings/DB2xml/
    • fhir/mappings/json2DB/
    • fhir/mappings/xml2DB/
  • As you can probably tell by the folder names, there is a folder for each “direction” of data mapping (to and from the database) in both JSON and XML.
  • Within each folder there is a file for each type of supported resource that can be mapped on the server. Each file is aptly named after the resource name is represents. In our server’s case, there is a file named Patient.lua within each folder.
  • Each file contains a function that defines the mapping logic for that direction of mapping. These functions will perform any relevant parameter validation, as well as:
    • Create the necessary FHIR objects (see the previous subsection FHIR Resource Profiles and Objects) , which is done when performing a SEARCH or READ operation on the server. Relevant mapping functions that are specific to our database schema will map the populated database result set into this newly created FHIR object which will be returned.
    • Create database table objects (the database schema for this server is defined in other/FHIR_VMDs/patient.vmd),  which is done when mapping to the database during UPDATE and CREATE operations.  Relevant mapping functions that are specific to our database schema will map all of the data from the FHIR object (that was sent in the body of the POST/PUT request) to the database table object. The validation code will make sure that all the required fields are present in the FHIR object.

Adapting the FHIR Server [top]

The FHIR Server code structure was designed to be very modular to make it easy to navigate and understand what the different pieces are doing. You may want to review How the FHIR Server works to help you with adapting the code.

Adding a new FHIR Resource:

  • As you observed in FHIR Resource Profiles and Objects, in order for the server to create new resource objects it needs the FHIR resource profile. These profiles can be obtained from www.hl7.org/fhir.
  • Once you have this, create a new file using the name of the resource in fhir/resources/definitions/types/
  • Store the resource profile string in a new object in this newly created file as a JSON or XML attribute (look in the existing Patient.lua resource files for an example on how to do this).
    • Make sure the JSON or XML is valid first using a validator (http://jsonlint.com/ or http://www.xmlvalidation.com/). The HL7.org site sadly doesn’t always provide completely error free parseable XML and JSON for these profile strings.
    • If using XML, remove the text node located at StructureDefinition.text which is just an HTML visual representation of the resource profile that is unneeded and will just increase parsing time.
  • Add the new resource to fhir/resources/definitions/controller.lua (follow the format of the existing entries already in the file.)
  • At this point, the server will be able to create new FHIR objects using the newly defined resource as outlined in the FHIR Resource Profiles and Objects section. HOWEVER you still need to specify new mappings for that resource in order for the server to write to and read from the database…

Creating Mappings For New FHIR Resources:

  • Once a new resource has been added to the server (see previous section) then you are ready to create new mappings so that the resource can be mapped to and from the database.
  • A new file will need to be created within each mapping folder in fhir/mappings/. (so the folders DB2json, DB2xml, json2DB, and xml2DB). The name of the file created must match that of the new resource name.
  • Within each file, you will need to create a function that will carry out the direction of mapping as indicated by the folder name. So for a new file created in json2DB, you will need to have the function map a JSON FHIR object to a database table object, whose structure is defined as per an uploaded VMD (like the server already does with other/FHIR_VMDs/patient.vmd for patient resources)
  • Make sure you include validation in your mapping functions so that when new resource entities are being created/updated, the proper information that is required by your database is being included.
  • After these files have been created, the controller fhir/mappings/controller.lua will need to be updated with the new mappings.
  • Refer to the existing Patient.lua files within the mapping folders, as well as the existing controller entries, for examples.

Creating  Different Types of Mappings:

In the current setup, the server maps from FHIR to a simple sqlite database setup and vice versa. However, this may not always be the case in a real world scenario. Here are some tips on how to go about implementing different kinds of mappings:

  • New Database/Different Schema
    • If you wanted to create mappings to a new database you would only need to update the logic in the connection and query functions in database.lua.
    • The logic in the mappings functions in fhir/mappings/… should still work as normal assuming the schema stays is the same.
    • If your schema is different, then the mapping functions would need to be updated in fhir/mappings/(DB2json, DB2xml, json2DB, xml2DB). You would also need to update the existing validation code depending on the fields the new schema has.
    • If the new schema uses resources other than the ones already defined, you would also need to add new resources and mappings as explained above.
  • Other than Database
    • Some setups might map to something completely different from databases, such as HL7.
    • In order to do this you would need to create entirely new mapping folders. (DB2json, DB2xml, json2DB, xml2DB)
    • You would need two new mapping folders for each new mapping type, one folder for each mapping direction. (for example, HL72json and json2HL7).
    • For each resource type you have, you would need to create a new mapping file within them (as was described for new database mappings above).
    • Also, the controller file (fhir/mappings/controller.lua) would need to be updated with the new mapping types you created.
    • The functions in fhir/mappings/utilities.lua would also need to be updated to cope with new mapping types. So instead of functions like MappingUtils.getFhirFromDbResults() you would have something like MappingUtils.getFhirFromHL7() for example, which would be able to programmatically select the proper mapping function as defined on the mappings controller (like it is doing currently in MappingUtils.getFhirFromDbResults()).
    • Lastly, some of the code in the request handlers in web/handlers/types/ would need to be changed to deal with HL7 (or whatever new kind of mapping you are doing). The database logic could be removed, and the new calls into your mapping utility functions would need to be put in.

More information [top]

 

One Comment

Leave A Comment?