...
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.
...
OpenMRS Web Services
In the past, people have written several web service modules for OpenMRS, but they've never had enough high-level design or buy-in.
Now, for the first time, we're 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.
Caveat: we have very little expertise with web services.
We did a first pass at designing a web service API that was going to be exposed via both REST and SOAP using Apache CXF. We talked to Ola Bini a month ago and he told us (nicely) that this was all wrong, and we needed to learn about REST, resources, and representations. So we bought a couple copies of REST in Practice, and completely re-did our design. We feel pretty happy with our new design, but we really need expert feedback to tell us whether or not we're on the right track, and what we should do differently. Hopefully this time the answer isn't "everything". :-)
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
How much Hypermedia?
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.5" of the Richardson Maturity Model (described by Martin Fowler here). Resources will include links to URIs of other resources, but not phrased as "rel"s. (We want to support json, we and we're not describing meaningful business-process in those links.)
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?
Standard CRUD pattern as applied to our API
We'll be following (mostly) standard practice with HTTP methods for domain object CRUD:
Action | URI | Outcomes |
---|---|---|
Search | GET /ws/rest/patient?q=name+or+identifier |
...
success = 200 OK with a list of minimal patient representations as content | ||
Create | POST /ws/rest/patient | success = 201 CREATED; Location: "uri-of-resource", content: default rep of created patient |
Retrieve | GET /ws/rest/patient/ |
...
uuid.json | success: 200 OK; content: default json representation of patient |
Update | POST /ws/rest/patient/ |
...
uuid |
...
"1978-05-24 |
...
", "birthdateEstimated": false } | success = 204 NO CONTENT |
Soft Delete | DELETE /ws/rest/patient/ |
...
uuid | success = 204 NO CONTENT |
Hard Delete | DELETE /ws/rest/patient/ |
...
uuid |
...
?purge=true |
...
success = 204 NO CONTENT |
Questions:
- Is it okay to use a POST to update only parts of a resource (instead of PUT with the complete object)?
- Is there a better way to expose the rarely-used Hard Delete method?
Multiple representations (minimum, default, full, ...)
Our domain objects are often large (e.g. a patient and all their encounters and clinical observations), 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-sized versions of any resource. For example:
URI | Content |
---|---|
/ws/rest/patient/uuid | default representation (not including audit info, including the patient's preferred name but not aliases, etc) |
/ws/rest/patient/uuid?v=full | representation including all audit info, all names, all historic addresses, etc. |
/ws/rest/patient/uuid?v=fullwithencounters | full representation plus summaries of all the patient's encounters (this can get very big) |
/ws/rest/patient/uuid?v=ref | minimal representation, just containing a displayable string, the uri of the default rep, and the uuid |
Question: Is this appropriate? If so, what's the right terminology for this? If not, what alternate approach should we be taking?
Usually the default representation of a resource will include "ref"-sized versions of other resources, while the full representation includes "default"-sized versions of other resources. For example a patient would include
default | full |
---|---|
{ |
...
|
...
: |
...
"1050 |
...
Wishard |
...
Blvd., |
...
RG5, |
...
Indianapolis, |
...
IN", |
...
|
...
: |
...
"http://.../ws/rest/person/puuid/ |
...
address/3350d0b5-821c-4e5e-ad1d-a9bce331e118" |
...
|
...
|
...
more |
...
stuff |
...
*/ |
...
|
...
{ |
...
: |
...
{ |
...
|
...
: |
...
"3350d0b5-821c-4e5e-ad1d-a9bce331e118", |
...
|
...
: |
...
"1050 |
...
Wishard |
...
Blvd.", |
...
|
...
: |
...
"RG5", |
...
|
...
: |
...
"Indianapolis", |
...
|
...
: |
...
"IN", |
...
|
...
: |
...
"http://.../ws/rest/person/puuid/ |
...
address/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".
...
For example a patient has identifiers. These sub-resources will not get a top-level uri, rather:
list all identifiers for the given patient | GET /ws/rest/ |
...
patient/ |
...
uuid/identifiers | |
add an identifier to the given patient | POST /ws/rest/patient/ |
...
uuid/identifiers | |
fetch/update/delete a specific identifier | GET/POST/DELETE /ws/rest/patient/uuid/ |
...
identifiers/ |
...
identifier-uuid |
We'll lean towards making things top-level resources rather than sub-resources, for example:
- encounter will be a top-level resource (even though every encounter belongs to a patient) because you might want to search for encounters across different patients, like GET /ws/rest/encounter?from=2011-01-01&to=2011-01-31
- "enrollment" (e.g. the fact that a patient is enrolled in a program or study) will be top-level, instead of /patient/uuid/enrollments or /program/uuid/patients
Does this sound right?
Authentication
We assume this will be straightforward once we sit down to design it (though probably annoying to implement). Any words of warning? Any examples we should look at or obvious technology terms to search for?
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:
- PatientResource (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 (Searchable, Creatable, Retrievable, Updatable, Deletable, Purgeable), and via a base class that helps reflect these interfaces onto our pre-web-service domain objects: DelegatingCrudResource.