What is OpenMRS
OpenMRS is an electronic medical record platform. Basically we provide an API and data model for storing and analyzing patient-level data, and a reference application that's used in 40+ mostly-developing countries. The system includes "core" code and pluggable "modules" that allow very powerful extensions of functionality, and UI-level customization.
What about Web Services
In the past, people have written several web service modules for OpenMRS, but they've never had enough high-level design.
Now, for the first time, we're really dedicating our core development team to doing web services right. We need to build a module that exposes our API over web services, in a way that we will support going forwards, and with a design we're proud of.
Version 1 Requirements
We polled the broader OpenMRS community, and came up with a list of User Stories to include in Version 1 of the module. Very concisely they are:
- Get a "patient summary" (i.e. a big chunk of data about a patient, their clinical encounters, etc)
- Create a new encounter or edit an existing encounter for a patient (e.g. for mobile data entry)
- Manage and list a named group of patients (to support downloading summaries of "My patients" to a phone)
- Let an external laboratory system look up patients and metadata in OpenMRS (e.g. the lab system fetches a patient, location, and user so that it can create an encounter in OpenMRS)
- Migrate data from legacy systems
Design Thoughts and Questions
The API we are going to expose in our 1.0 pass at web services is entirely about doing CRUD on a data store. We're not dealing with any application flow or business logic. As such we will aim for level 2 of the Richardson Maturity Model (described by Martin Fowler here) and ignore the idea of embedding state transitions in our resources. Is this a mistake?
We will allow Spring to automatically transform our results to json or xml depending on the HTTP request. (This probably prevents us from having a proper DAP or XSD. Is this a mistake?)
We'll be following (mostly) standard practice with HTTP methods for domain object CRUD:
- Create: POST /ws/rest/patient
- Search: GET /ws/rest/patient?q=name+or+identifier
- Retrieve: GET /ws/rest/patient/uuid
- Update: POST /ws/rest/patient/uuid with the request content the properties we want to change on the object
- e.g. to change a patient's birthdate you'd POST
Unknown macro: { "birthdate"}
- does this seem okay, rather than using PUT with the complete object?
- e.g. to change a patient's birthdate you'd POST
- Delete: DELETE /ws/rest/patient/uuid
- this actually means "mark deleted" in OpenMRS
- "Delete forever": DELETE /ws/rest/patient/uuid?purge=true
- actually deletes an entity from the database
- is there a better way to indicate this special, rarely-used method?
Our domain objects are often large, and contain lots of rarely-interesting book-keeping information (creator, date created, etc), and we expect many clients to be bandwidth-limited. Therefore we want to allow clients to fetch different representations of a any resource. For example you could get a "default" patient representation (only including the patient's preferred name, not including audit info) or a "full" representation (including all their names, all audit info, etc). Both of these representations would live at the same URI. If the client wants something other than the default representation, they would communicate that by doing a GET with "Accept-Type: full". Is this appropriate? If so, what's the right terminology for this? If not, what alternate approach should we be taking?
We also introduce the idea of a "Ref", which is a minimal representation of any object, just containing its uuid, a displayable string, and a URI link to the full resource. So for example if I fetch the default representation of a patient, I might get this:
{ "personAddress" : { "uuid" : "3350d0b5-821c-4e5e-ad1d-a9bce331e118", "display" : "1050 Wishard Blvd., RG5, Indianapolis, IN", "uri" : "http://.../ws/rest/personaddress/3350d0b5-821c-4e5e-ad1d-a9bce331e118" }, /* more stuff */ }
Whereas if I fetch the full representation of a patient I might get:
{ "personAddress" : { "uuid" : "3350d0b5-821c-4e5e-ad1d-a9bce331e118", "address1" : "1050 Wishard Blvd.", "address2" : "RG5", "cityVillage" : "Indianapolis", "stateProvince" : "IN", "uri" : "http://.../ws/rest/personaddress/3350d0b5-821c-4e5e-ad1d-a9bce331e118" }, /* more stuff */ }
A more relevant example of this is that an encounter might contain 100 observations. The default encounter representation would just include a displayable "weight = 70", whereas the full encounter representation would include full details about the "weight" question (e.g. that it's unit is "kg", its range is 0-250, etc).
Is this okay, or is this some sort of anti-pattern that's going to get us into trouble?
We also have the idea of "sub-resources". Take the example of a "Program" (e.g. "Tuberculosis Treatment Program") and a "Program Enrollment" (i.e. a patient may be enrolled in a program from date X to date Y). Our plan is that:
- A patient's program enrollment has a top-level URI /ws/rest/enrollment/uuid (i.e. it's not included within the patient)
- To see what programs a patient is enrolled in: GET /ws/rest/patient/patient-uuid/enrollments
- To see what patients are enrolled in a program: GET /ws/rest/program/program-uuid/patients
- To enroll a patient in a program you can do either of:
- POST /ws/rest/patient/patient-uuid/enrollments // may omit the patient from the request body
- POST /ws/rest/program/program-uuid/patients // may omit the program from the request body
- POST /ws/rest/enrollment // must specify both patient and program in the request body
Does this sound right?
Some Java Code
We've tried to put together a framework that will make it very quick to expose each of our existing domain objects as resources in a standard way, while still giving us flexibility to step out of that pattern if need be.
To create and expose a resource you would have to write two classes:
- https://source.openmrs.org/browse/~br=trunk/Modules/webservices.rest/trunk/omod/src/main/java/org/openmrs/module/webservices/rest/web/resource/PatientResource.java?hb=truePatientResource (contains one-line implementations of getByUniqueId, save, etc, and descriptions of the available representations)
- PatientController (contains short methods to connect Spring MVC's request handling to PatientResource)
We've tried to standardize the way we do CRUD via interfaces (Creatable, Retrievable, Updatable, Deletable, Purgeable), and via a base class that helps reflect these interfaces onto our pre-web-service domain objects: DelegatingCrudResource.