JSON API cheatsheet

Introduction

Properly organising API endpoints and JSON-serialized data responses it's pretty difficult, especially when you work on a project that will extended and improved by a larger community.

I'm getting used to stick to the JSON-API - version 1.0, considered stable at the time of writing - when defining APIs. Few conventions make your API clearer and many libraries also support serializing and deserializing JSON APIs with ease.

JSON:API is a specification for how a client should request that resources be fetched or modified, and how a server should respond to those requests.

JSON:API is designed to minimize both the number of requests and the amount of data transmitted between clients and servers. This efficiency is achieved without compromising readability, flexibility, or discoverability.

Adopting the full standard is maybe too much for this project.

In short the API must respect few conventions:

  • Content-type must always be Content-Type: application/vnd.api+json
  • JSON Payloads have a standard structure, see section below
  • Responses use JSON but add typing checks i.e. JSON Schema types
  • Relationships are modelled in the response
  • Standard error codes

Request and response headers

Request

GET /articles HTTP/1.1
Accept: application/vnd.api+json

Response

HTTP/1.1 201 Created
Content-Type: application/vnd.api+json

Response codes

Code Verbose Description
200 OK resource / relationship fetched correctly
404 Not found resource / relationship not found
201 Created POST request created a resource
202 Accepted POST request accepted but not yet processed
403 Forbidden unsupported request
409 Conflict duplicate operation with the same client id

Resource identifier

Any resource must contain:

  • id
  • type

and optionally

  • attributes
  • relationships
{
  "type": "articles",
  "id": "1",
  "attributes": {
    "title": "Rails is Omakase"
  },
  "relationships": {
    "author": {
      "links": {
        "self": "/articles/1/relationships/author",
        "related": "/articles/1/author"
      },
      "data": { "type": "people", "id": "9" }
    }
  }
}

Resource structure

Responses contains always a toplevel JSON Object

And at least one of the following:

  • data: the document’s “primary data”
  • errors: an array of error objects
  • meta: a meta object that contains non-standard meta-information.

it may contain also:

  • jsonapi: an object describing the server’s implementation
  • links: a links object related to the primary data.
  • included: an array of resource objects that are related to the primary data and/or each other (“included resources”).

Relationships

Relationship resource objects must at least contain one of:

  • links: a links object containing at least one of the following:
    • self: a link for the relationship itself (a “relationship link”). This link allows the client to directly manipulate the relationship. For example, removing an author through an article’s relationship URL would disconnect the person from the article without deleting the people resource itself. When fetched successfully, this link returns the linkage for the related resources as its primary data. (See Fetching Relationships.)
    • related: a related resource link
  • data: resource linkage
  • meta: a meta object that contains non-standard meta-information about the relationship.
  {
  "type": "articles",
  "id": "1",
  "attributes": {
  "title": "Rails is Omakase"
  },
  "relationships": {
  "author": {
  "links": {
  "self": "http://example.com/articles/1/relationships/author",
  "related": "http://example.com/articles/1/author"
  },
  "data": { "type": "people", "id": "9" }
  }
  },
  "links": {
  "self": "http://example.com/articles/1"
  }
  }

Compound Documents

The "included" field in the resource description is used to add related resources in order to minimize the number of requests.

NOTE

Included resources must contain a links.self that is included somewhere else in the response resources.

// excerpt
{ "included": [{
    "type": "people",
    "id": "9",
    "attributes": {
      "first-name": "Dan",
      "last-name": "Gebhardt",
      "twitter": "dgeb"
    },
    "links": {
      "self": "http://example.com/people/9"
    }
  }, {
    "type": "comments",
    "id": "5",
    "attributes": {
      "body": "First!"
    },
    "relationships": {
      "author": {
        "data": { "type": "people", "id": "2" }
      }
    },
    "links": {
      "self": "http://example.com/comments/5"
    }
  }]
}
/// end excerpt

Accessing collections

GET /articles
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
  "links": {
    "self": "http://example.com/articles"
  },
  "data": [
    {
      "type": "articles",
      "id": "1",
      "attributes": {
        "title": "JSON:API paints my bikeshed!"
      }
    },
    {
      "type": "articles",
      "id": "2",
      "attributes": {
        "title": "Rails is Omakase"
      }
    }
  ]
}

Pagination

Links are used for pagination, containing resources expressed as urls

  • first: the first page of data
  • last: the last page of data
  • prev: the previous page of data
  • next: the next page of data

The value null must be used if the specific key is not available

The page query parameter should be used for pagination. Also ok page[per_page] etc.

Filtering

The filter query parameter should be used. Nothing else is specified.

Accessing individual resources

GET /articles/1
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
  "links": {
    "self": "http://example.com/articles/1"
  },
  "data": {
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON:API paints my bikeshed!"
    },
    "relationships": {
      "author": {
        "links": {
          "related": "http://example.com/articles/1/author"
        }
      }
    }
  }
}

Accessing relationships

GET /articles/1/relationships/comments HTTP/1.1
Accept: application/vnd.api+json
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
  "links": {
    "self": "/articles/1/relationships/author",
    "related": "/articles/1/author"
  },
  "data": {
    "type": "people",
    "id": "12"
  }
}

Creating resources

POST /photos HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
  "data": {
    "type": "photos",
    "attributes": {
      "title": "Ember Hamster",
      "src": "http://example.com/images/productivity.png"
    },
    "relationships": {
      "photographer": {
        "data": { "type": "people", "id": "9" }
      }
    }
  }
}

Updating resources

PATCH /articles/1 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
  "data": {
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "To TDD or Not"
    }
  }
}

Updating relationships

PATCH /articles/1 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
  "data": {
    "type": "articles",
    "id": "1",
    "relationships": {
      "author": {
        "data": { "type": "people", "id": "1" }
      }
    }
  }
}

Updating relationships - alternative way

One-to-one

PATCH /articles/1/relationships/author HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
  "data": { "type": "people", "id": "12" }
}

One-to-many and Many-to-many

PATCH /articles/1/relationships/tags HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
  "data": [{ "type": "tags", "id": "2" }, { "type": "tags", "id": "3" }]
}

Deleting relationships

DELETE /articles/1/relationships/comments HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
  "data": [
    { "type": "comments", "id": "12" },
    { "type": "comments", "id": "13" }
  ]
}

Errors

Errors must be returned as an Array errors key on the top-level object.

Each error can include:

  • id: optional unique identifier
  • status: HTTP status code
  • code
  • title
  • detail
  • links
    • about: link to the page describing this specific error

References