Using JSONAPI as a Web API format for CubicWeb

Following the introduction post about rethinking the web user interface of CubicWeb, this article will address the topic of the Web API to exchange data between the client and the server. As mentioned earlier, this question is somehow central and deserves particular interest, and better early than late. Of the two candidate representations previously identified Hydra and JSON API, this article will focus on the later. Hopefully, this will give a better insight of the capabilities and limits of this specification and would help take a decision, though a similar experiment with another candidate would be good to have. Still in the process of blog driven development, this post has several open questions from which a discussion would hopefully emerge...

A glance at JSON API

JSON API is a specification for building APIs that use JSON as a data exchange format between clients and a server. The media type is application/vnd.api+json. It has a 1.0 version available from mid-2015. The format has interesting features such as the ability to build compound documents (i.e. response made of several, usually related, resources) or to specify filtering, sorting and pagination.

A document following the JSON API format basically represents resource objects, their attributes and relationships as well as some links also related to the data of primary concern.

Taking the example of a Ticket resource modeled after the tracker cube, we could have a JSON API document formatted as:

GET /ticket/987654 Accept: application/vnd.api+json

{ "links": { "self": "https://www.cubicweb.org/ticket/987654" }, "data": { "type": "ticket", "id": "987654", "attributes": { "title": "Let's use JSON API in CubicWeb" "description": "Well, let's try, at least...", }, "relationships": { "concerns": { "links": { "self": "https://www.cubicweb.org/ticket/987654/relationships/concerns", "related": "https://www.cubicweb.org/ticket/987654/concerns" }, "data": {"type": "project", "id": "1095"} }, "done_in": { "links": { "self": "https://www.cubicweb.org/ticket/987654/relationships/done_in", "related": "https://www.cubicweb.org/ticket/987654/done_in" }, "data": {"type": "version", "id": "998877"} } } }, "included": [{ "type": "project", "id": "1095", "attributes": { "name": "CubicWeb" }, "links": { "self": "https://www.cubicweb.org/project/cubicweb" } }] }

In this JSON API document, top-level members are links, data and included. The later is here used to ship some resources (here a "project") related to the "primary data" (a "ticket") through the "concerns" relationship as denoted in the relationships object (more on this later).

While the decision of including or not these related resources along with the primary data is left to the API designer, JSON API also offers a specification to build queries for inclusion of related resources. For example:

GET /ticket/987654?include=done_in Accept: application/vnd.api+json

would lead to a response including the full version resource along with the above content.

Enough for the JSON API overview. Next I'll present how various aspects of data fetching and modification can be achieved through the use of JSON API in the context of a CubicWeb application.

CRUD

CRUD of resources is handled in a fairly standard way in JSON API, relying of HTTP protocol semantics.

For instance, creating a ticket could be done as:

POST /ticket Content-Type: application/vnd.api+json Accept: application/vnd.api+json

{ "data": { "type": "ticket", "attributes": { "title": "Let's use JSON API in CubicWeb" "description": "Well, let's try, at least...", }, "relationships": { "concerns": { "data": { "type": "project", "id": "1095" } } } } }

Then updating it (assuming we got its id from a response to the above request):

PATCH /ticket/987654 Content-Type: application/vnd.api+json Accept: application/vnd.api+json

{ "data": { "type": "ticket", "id": "987654", "attributes": { "description": "We'll succeed, for sure!", }, } }

Relationships

In JSON API, a relationship is in fact a first class resource as it is defined by a noun and an URI through a link object. In this respect, the client just receives a couple of links and can eventually operate on them using the proper HTTP verb. Fetching or updating relationships is done using the special <resource url>/relationships/<relation type> endpoint (self member of relationships items in the first example). Quite naturally, the specification relies on GET verb for fetching targets, PATCH for (re)setting a relation (i.e. replacing its targets), POST for adding targets and DELETE to drop them.

GET /ticket/987654/relationships/concerns Accept: application/vnd.api+json

{ "data": { "type": "project", "id": "1095" } }

PATCH /ticket/987654/relationships/done_in Content-Type: application/vnd.api+json Accept: application/vnd.api+json

{ "data": { "type": "version", "id": "998877" } }

The body of request and response of this <resource url>/relationships/<relation type> endpoint consists of so-called resource identifier objects which are lightweight representation of resources usually only containing information about their "type" and "id" (enough to uniquely identify them).

Related resources

Remember the related member appearing in relationships links in the first example?

[ ... ] "done_in": { "links": { "self": "https://www.cubicweb.org/ticket/987654/relationships/done_in", "related": "https://www.cubicweb.org/ticket/987654/done_in" }, "data": {"type": "version", "id": "998877"} } [ ... ]

While this is not a mandatory part of the specification, it has an interesting usage for fetching relationship targets. In contrast with the .../relationships/... endpoint, this one is expected to return plain resource objects (which attributes and relationships information in particular).

GET /ticket/987654/done_in Accept: application/vnd.api+json

{ "links": { "self": "https://www.cubicweb.org/998877" }, "data": { "type": "version", "id": "998877", "attributes": { "number": 4.2 }, "relationships": { "version_of": { "self": "https://www.cubicweb.org/998877/relationships/version_of", "data": { "type": "project", "id": "1095" } } } }, "included": [{ "type": "project", "id": "1095", "attributes": { "name": "CubicWeb" }, "links": { "self": "https://www.cubicweb.org/project/cubicweb" } }] }

Meta information

The JSON API specification allows to include non-standard information using a so-called meta object. This can be found in various place of the document (top-level, resource objects or relationships object). Usages of this field is completely free (and optional). For instance, we could use this field to store the workflow state of a ticket:

{ "data": { "type": "ticket", "id": "987654", "attributes": { "title": "Let's use JSON API in CubicWeb" }, "meta": { "state": "open" } }

Permissions

Permissions are part of metadata to be exchanged during request/response cycles. As such, the best place to convey this information is probably within the headers. According to JSON API's FAQ, this is also the recommended way for a resource to advertise on supported actions.

So for instance, response to a GET request could include Allow headers, indicating which request methods are allowed on the primary resource requested:

GET /ticket/987654 Allow: GET, PATCH, DELETE

An HEAD request could also be used for querying allowed actions on links (such as relationships):

HEAD /ticket/987654/relationships/comments Allow: POST

This approach has the advantage of being standard HTTP, no particular knowledge of the permissions model is required and the response body is not cluttered with these metadata.

Another possibility would be to rely use the meta member of JSON API data.

{ "data": { "type": "ticket", "id": "987654", "attributes": { "title": "Let's use JSON API in CubicWeb" }, "meta": { "permissions": ["read", "update"] } } }

Clearly, this would minimize the amount client/server requests.

More Hypermedia controls

With the example implementation described above, it appears already possible to manipulate several aspects of the entity-relationship database following a CubicWeb schema: resources fetching, CRUD operations on entities, set/delete operations on relationships. All these "standard" operations are discoverable by the client simply because they are baked into the JSON API format: for instance, adding a target to some relationship is possible by POSTing to the corresponding relationship resource something that conforms to the schema.

So, implicitly, this already gives us a fairly good level of Hypermedia control so that we're not so far from having a mature REST architecture according to the Richardson Maturity Model. But beyond these "standard" discoverable actions, the JSON API specification does not address yet Hypermedia controls in a generic manner (see this interesting discussion about extending the specification for this purpose).

So the question is: would we want more? Or, in other words, do we need to define "actions" which would not map directly to a concept in the application model?

In the case of a CubicWeb application, the most obvious example (that I could think of) of where such an "action" would be needed is workflow state handling. Roughly, workflows in CubicWeb are modeled through two entity types State and TrInfo (for "transition information"), the former being handled through the latter, and a relationship in_state between the workflowable entity type at stake and its current State. It does not appear so clearly how would one model this in terms of HTTP resource. (Arguably we wouldn't want to expose the complexity of Workflow/TrInfo/State data model to the client, nor can we simply expose this in_state relationship, as a client would not be able to simply change the state of a entity by updating the relation). So what would be a custom "action" to handle the state of a workflowable resource? Back in our tracker example, how would we advertise to the client the possibility to perform "open"/"close"/"reject" actions on a ticket resource? Open question...

Request for comments

In this post, I tried to give an overview of a possible usage of JSON API to build a Web API for CubicWeb. Several aspects were discussed from simple CRUD operations, to relationships handling or non-standard actions. In many cases, there are open questions for which I'd love to receive feedback from the community. Recalling that this topic is a central part of the experiment towards building a client-side user interface to CubicWeb, the more discussion it gets, the better!

For those wanting to try and play themselves with the experiments, have a look at the code. This is a work-in-progress/experimental implementation, relying on Pyramid for content negotiation and route traversals.

What's next? Maybe an alternative experiment relying on Hydra? Or an orthogonal one playing with the schema client-side?