For as long as I can remember, developing JSON APIs has been a bit of a free for all. We've had the power to create complex APIs quickly, but typically every API follows its own standards. There've been some unwritten rules and sharing of ideas, but never has there been such a complete specification as the one that the team over at OpenAPI has developed. Let's explore what OpenAPI has to offer and why I welcome this specification so enthusiastically.
What is the JSON API spec?
For the longest time, JSON APIs have been implemented in vastly differing structures. One API would look completely different to the next, and sometimes they shared similarities like using /api/v1 for versioning or params being structured with the model as the root node e.g. { base_node: { foo: "bar" } }, but on the whole, that's where the similarities ended. Each project, sometimes each team, would create their own standards. At worst, each endpoint would have its own format. This fragmentation is costly - with a specification we know what to expect from every endpoint from any application. This allows us to build up transferable knowledge and time saving, bug-averse libraries.
In 2013 Yehuda Katz decided to take a shot at drafting a spec for JSON APIs. The first draft was extracted from the JSON transport implicitly defined by Ember Data's REST adapter. In 2015 the spec reached a stable version 1.0 in May 2015, since then numerous contributors have been discussing and development is happening at a steady pace. The spec will hopefully soon reach v1.1, you can view the changes here https://jsonapi.org/#update-history
Getting started
All requests should be submitted with the Content-Type and Accepts headers with the application/vnd.api+json MIME type
To create a new resource we should submit a POST request which includes a json document in the following format.
As you can see the document includes a data node at the root which defines an individual resource object. The resource object defines the type, the attributes and the relationships. We can then use this information on the backend to generate the correct object in the database. When creating a resource it's possible for the client to provide a UUID in the data.attributes.id field to specify the ID of the new resource.
Currently only one operation per request is permitted but there are discussions around how to implement multiple operations per request in the future which will allow us to create relationships at the same time as creating a resource. This will be included in future versions of the specification.
The spec also tells us a few things about how we should format the responses to our requests.
If the resource was successfully created and the client provided an ID, we can return a 204 No Content response which reduces traffic. If the resource was successfully created but no ID was provided, we must return a 201 including a Location header and a document containing the newly created resource, e.g.
HTTP/1.1 201 Created
Location: https://example.com/foos/ddf358c2-a0fb-4035-946d-4b8f96ea334b
Content-Type: application/vnd.api+json
As you can see, creating resources is fairly straightforward. Now that this is standardized we can create some boilerplate code which we can reuse in all of our applications. However, this isn't where the JSON API spec really shines - fetching data is where the real improvements are gained!
Fetching resources and relationships
The only difference between individual and collection responses is the contents of the data node. Collection endpoints return an array of resource objects while an individual endpoint would return an individual resource object, e.g.
As you can see in the collection example, the articles are written by the same author. A link is provided to fetch the author which allows us to load and cache the author once on the client. We can alternatively ask the backend to include the author relationships in the response.
Including relationships
To include a relationship we need to supply the list of relationships we want to return in the query params when making the request, e.g.
/articles/1?include=authors
This will return the relationship in a slightly different format:
With a singular resource this might seem overkill, but when you're returning 30 records that share a lot of data, this really reduces traffic. This approach also gives the client much more power in terms of defining exactly what they want.
Allowing the clients to request a subset of fields with sparse fieldsets
JSON API spec also defines a mechanism to allow the client to reduce the amount of fields to be returned for a resource. To request a sparse fieldset we can pass the fields through the query parameters once again. We can even pass them for relationships:
This article just touches the surface of what is defined in the spec. It's really worth taking a look into - it covers pagination, error formatting, response types, creating and updating resources, sorting results and much more. You can read the full spec on the official website https://jsonapi.org/
More and more companies are getting on board with the JSON API spec. Netflix have developed fast_jsonapi and released it to the open source community. Many other gems exist too, active_model_serializers now supports the JSON API spec. I'm hopeful that many more companies will adopt the spec and we can make it a standard. Rails seems to be moving towards adopting it as the default for API mode. The future looks very positive.