Proposal: Changes to REST module to improve typings

I want to propose a few changes to the REST module. Most of these are motivated by trying to improve the typings and documentations of our REST APIs, although some of them are miscellaneous. I don’t think any of the proposed changes breaks backwards compatibility.

Explicitly specify search handler id in URL when making search requests

Almost every REST resource provides a route to do a search. For example GET /ws/rest/v1/encounter?patient=<uuid> searches for encounters by patient uuid. We want to better document what params are accepted as search parameters, but this turns out to be difficult.

  • For a resource, it is possible to have multiple search handlers, and each search chandler can accept different parameters. A search handler is selected either by:
    • explicitly passing in a s= GET param to specify the search handler id. For example, GET /ws/rest/v1/encounter?patient=<uuid>8&obsConcept=<uuid>&s=byObs invokes the search handler with id byObs
    • implicitly infer which search handler to use based on the params that are passed in. For example, GET /ws/rest/v1/encounter?patient=<uuid>8&obsConcept=<uuid> will cause the REST module to do a best guess and find a search handler that handles the parameters patient and obsConcept. In this case, this also happens to be the byObs search handler.
  • It is possible that NO search handlers are selected based on the params passed in. In that case, some REST resources have their own search() function (via the Searchable interface), and that gets invoked. (Essentially this Searchable#search() function plays a similar role to SearchHandler, but it’s just not implemented as such). For example, GET /ws/rest/v1/encounter?patient=<uuid> fails the param criteria of the Encounter resources’ search handlers, but invokes Encounter.search() as a fallback.

The above logic is coded here.

I proposed that:

  • we add these routes to support explicitly selecting search handlers. This allows us to document which search handlers are available, and what parameters each search handler expects.
  • we make these routes available as POST requests in addition to GET. This doesn’t help with typings, but it solves the problem of URL character limits we run into when our GET params are too long, especially when we provide long v=custom:... strings. (FHIR does something similar)

So the new routes would look something like this:

  • GET /ws/rest/<resource>/search/ selects the SearchHandler with id "default"
  • POST /ws/rest/<resource>/search/ (same as GET)
  • GET /ws/rest/<resource>/search/<searchHandlerid> selects the SearchHandler with the given searchHandlerId
  • POST /ws/rest/<resource>/search/<searchHandlerid> (same as GET)

SearchHandler, compared to Searchable#search(), has an additional advantage of needing to declare its required and optional parameters (example). While we still need to inspect the code to determine the typing of each parameter, they are easier to document that Searchable#search() (see this for comparison). Overtime, I think we should rewrite our REST resources to favor SearchHandlers over the search() / doSearch() functions in the Resource classes.

Explicitly return the representation of a resource.

When we fetch a resource, we can include a v= parameter specify its representation. For most resources, we can do something simple, like v=default, v=full, or provide a custom representation string like v=custom:(uuid,display). It is often the case that the returned resource transitively include other resources as well. For example, when fetching a Visit resource with default rep, it also transitively returns its patient, location, and encounters. Here is a sample (some fields omitted):

{
  "uuid": "c601d849-7f3a-4e8f-b006-4556887013ff",
  "display": "Home Visit @ Site 42 - 03/09/2025 06:19 PM",
  "patient": {
    "uuid": "1336ec3e-7ea9-4883-8a5b-f0332b9e84d8",
    "display": "10001F0 - Sandra Walker",
  },
  "visitType": {
    "uuid": "d66e9fe0-7d51-4801-a550-5d462ad1c944",
    "display": "Home Visit",
  },
  "indication": null,
  "location": {
    "uuid": "92dbdbdf-17da-4cf0-873c-ad15dfae71cb",
    "display": "Site 42",
  },
  "startDatetime": "2025-03-09T18:19:21.000+0000",
  "stopDatetime": "2025-03-09T18:41:21.000+0000",
  "encounters": [
    {
      "uuid": "f616575b-2415-49df-9c57-e915f238dffa",
      "display": "Lab Results 03/09/2025",
    },
  ],
  "resourceVersion": "1.9"
}

Currently, our documentation is not mature enough to specify the presentation for the transitively fetched resources. (Most of them should be of ref representation, but there is no guarantee.) I proposed that, for each resource, we explicitly include an extra field to specify its representation. This allows us to better discern the typings of the resources that are returned. For example, the above JSON would be augmented to this:

{
  "uuid": "c601d849-7f3a-4e8f-b006-4556887013ff",
  "display": "Home Visit @ Site 42 - 03/09/2025 06:19 PM",
  "rep": "default", // could also be "full", "custom" or other named representations
  "patient": {
    "uuid": "1336ec3e-7ea9-4883-8a5b-f0332b9e84d8",
    "display": "10001F0 - Sandra Walker",
    "rep": "ref"
  },
  "visitType": {
    "uuid": "d66e9fe0-7d51-4801-a550-5d462ad1c944",
    "display": "Home Visit",
    "rep": "ref"
  },
  "indication": null,
  "location": {
    "uuid": "92dbdbdf-17da-4cf0-873c-ad15dfae71cb",
    "display": "Site 42",
    "rep": "ref"
  },
  "startDatetime": "2025-03-09T18:19:21.000+0000",
  "stopDatetime": "2025-03-09T18:41:21.000+0000",
  "encounters": [
    {
      "uuid": "f616575b-2415-49df-9c57-e915f238dffa",
      "display": "Lab Results 03/09/2025",
      "rep": "ref"
    },
  ],
  "resourceVersion": "1.9"
}

This will bloat the returns result by a bit (< 20 bytes per resource), but this should small enough to be acceptable.

Allow specifying returned data format (XML vs JSON) as a GET / POST param

Currently, the data format returned by the REST module is specified using the "Content-Type" HTTP request header. A request with "Content-Type: application/json; will return JSON data as response. If the header is not specified, it returns XML by default.

I propose that we add a new parameter format= in our GET / POST request as an additional way to specify the returned data format. (FHIR does something similar with the _format param). This just mostly helps with development / debugging.

3 Likes

Had a discussion about this during the platform call today (thanks everyone for their feedback). Feedback from the meeting:

  • Explicitly specify search handler id in URL when making search requests

    • It probably makes sense to add those new routes to specify search handlers

      GET /ws/rest/<resource>/search/
      GET /ws/rest/<resource>/search/<searchHandlerId
      
    • It also probably makes sense to support searching via POST requests as well, although the URL for the POST requests need to differ from the GET routes slightly because we use the routes (but not also the HTTP methods) to determine the Controller to handle routing.

  • Explicitly return the representation of a resource.

    • The problem of not being knowing the representation (and therefore the fields) of child Resources returned should hopefully be fixed when we have better OpenAPI typings + documentation. Let’s not implement this for now.
  • Allow specifying returned data format (XML vs JSON) as a GET / POST param

    • It should be fine to add a param to specify the return data format. Maybe name it _format= to make it more FHIR-like.

One thing from above that I forgot to mention in the meeting was that I want push us to move away from implementing Resource#doSearch() in favor of using SearchHandlers instead (since Resource#doSearch() is even less type-strict than SearchHandlers). I’m interested in thoughts on that.

Thanks!

2 Likes