Standardizing RESTful JSON APIs with OpenAPI Spec

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

Beautiful designs are everywhere, even in API architecture

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.

{
  "data": {
    "type": "foo",
    "attributes": {
      "title": "Foo resource",
      "url": "https://example.com/bar.html"
    },
    "relationships": {
      "bar": {
        "data": {
          "type": "bar",
          "id": "9"
        }
      }
    }
  }
}

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

{
  "data": {
    "type": "foo",
    "id": "ddf358c2-a0fb-4035-946d-4b8f96ea334b",
    "attributes": {
      "title": "Foo resource",
      "url": "https://example.com/bar.html"
    },
    "links": {
      "self": "https://example.com/foos/ddf358c2-a0fb-4035-946d-4b8f96ea334b"
    }
  }
}

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.
{
  "data": {
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON:API paints my bikeshed!"
    }
  }
}
vs
{
  "data": [{
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON:API paints my bikeshed!"
    },
    "relationships": {
      "author": {
        "links": {
          "related": "https://example.com/articles/1/author"
        }
      }
    }
  }, {
    "type": "articles",
    "id": "2",
    "attributes": {
      "title": "Rails is Omakase"
    },
    "relationships": {
      "author": {
        "links": {
          "related": "https://example.com/articles/1/author"
        }
      }
    }
  }]
}
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.

Please sir, I want some more

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:

{
  "data": {
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON:API paints my bikeshed!",
      "body": "Lorem ipsum",
      "published-at": "2020-20-01T00:00:00+7:00"
    },
    "relationships": {
      "author": {
        "links": {
          "self": "http://example.com/articles/1/relationships/author",
          "related": "http://example.com/articles/1/author"
        },
        "data": { "type": "people", "id": "9" }
      }
    }
  },
  "included": [{
    "type": "people",
    "id": "9",
    "attributes": {
      "first-name": "Dan",
      "last-name": "Gebhardt",
      "twitter": "dgeb"
    },
    "links": {
      "self": "http://example.com/people/9"
    }
  }
}

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:

/articles?include=author&fields[articles]=title,body&fields[people]=name

This request would return the following:

{
  "data": {
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON:API paints my bikeshed!",
      "body": "Lorem ipsum",
    },
    "relationships": {
      "author": {
        "links": {
          "self": "http://example.com/articles/1/relationships/author",
          "related": "http://example.com/articles/1/author"
        },
        "data": { "type": "people", "id": "9" }
      }
    }
  },
  "included": [{
    "type": "people",
    "id": "9",
    "attributes": {
      "name": "D. Gebhardt"
    },
    "links": {
      "self": "http://example.com/people/9"
    }
  }
}

Wrapping up


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.

Like 2 likes
Joe Woodward
I'm Joe Woodward, a Ruby on Rails fanatic working with OOZOU in Bangkok, Thailand. I love Web Development, Software Design, Hardware Hacking.
Share:

Join the conversation

This will be shown public
All comments are moderated

Get our stories delivered

From us to your inbox weekly.