JSON API, wat is dat?

Kennis
Björn Brala
18 oktober 2018

Dit is de Nederlandse versie van een gastpost op Laravel News.

Yehuda Katz stelde de JSON API-specificatie voor het eerst op in mei 2013, twee jaar later werd ‘ie stabiel verklaard. Hoofddoel: efficiënte API-verzoeken maken. Je verzamelt er namelijk exact de data mee die je nodig hebt, simpelweg door aan te passen welke attributen of relaties je ophaalt. Resultaat: aanzienlijk minder data en verzoeken.

JSON API logo

{json:api}

De basis van JSON API-documenten is hetzelfde als die van andere formaten: je stuurt een verzoek naar het endpoint van de API en krijgt een document terug. De JSON API-specificatie beschrijft hoe de resources zijn gestructureerd. Dit helpt je om de API op een consistente manier aan te kunnen spreken zonder na te hoeven kijken naar de specifieke implementatie.

Laten we beginnen met een simpel voorbeeld. We doen een verzoek naar de `articles` resource met een simpele `GET`-request.

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

De server antwoordt min of meer dit:

// ...
{
  "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"
  }
} // ...

Dat ziet er best makkelijk uit. De `author`-relatie bevat een link naar de relatie en wat basisinformatie over de relatie. Via de links haal je gerelateerde resources op. Dit is maar een basisvoorbeeld. De specificatie heeft veel meer en diepgaandere features waardoor het een feest is om ermee te werken.

Compound documents

To reduce the number of HTTP requests, servers MAY allow responses that include related resources along with the requested primary resources. Such responses are called "compound documents".

 

Een compound-document is een resource die ook de data van zijn relaties bevat. Dit betekent dat een `article` – behalve zijn eigen data – ook die van de `author` kan bevatten. Je hoeft dus geen tweede verzoek naar de API te sturen om de auteur op te halen.

Deze feature maakt deze specificatie zo goed. Alle data van een resource in één keer ophalen, dat zie je bijna nooit bij andere API's. Tenzij daar specifiek code voor is geschreven. Bovendien maakt deze structuur het erg makkelijk voor de server om caching en – belangrijker nog – cache-invalidatie te organiseren.

Laten we eens kijken wat er gebeurt als we een compound-document ontvangen.

{
  "data": [{
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON API paints my bikeshed!"
    },
    "links": {
      "self": "http://example.com/articles/1"
    },
    "relationships": {
      "author": {
        "links": {
          "self": "http://example.com/articles/1/relationships/author",
          "related": "http://example.com/articles/1/author"
        },
        "data": {
          "type": "people",
          "id": "9"
        }
      },
      "comments": {
        "links": {
          "self": "http://example.com/articles/1/relationships/comments",
          "related": "http://example.com/articles/1/comments"
        },
        "data": [{
          "type": "comments",
          "id": "5"
        }, {
          "type": "comments",
          "id": "12"
        }]
      }
    }
  }],
  "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"
    }
  }, {
    "type": "comments",
    "id": "12",
    "attributes": {
      "body": "I like XML better"
    },
    "relationships": {
      "author": {
        "data": {
          "type": "people",
          "id": "9"
        }
      }
    },
    "links": {
      "self": "http://example.com/comments/12"
    }
  }]
}

Het antwoord van de server bevat een `included` eigenschap: een lijst met gerelateerde resources bij het artikel. Alle resources hebben een `type`-eigenschap. Hierin staat om welk type resource het gaat, inclusief een link naar het endpoint voor die resource.

In eerste instantie voelt het misschien raar om in het `article`-document te refereren naar de relaties met alleen een `id` en `type`, en de data van deze relaties in een losse eigenschap `included` te zoeken. Het punt is dat wanneer we in een verzoek meerdere artikelen ophalen die door dezelfde persoon zijn geschreven, deze data dan maar één keer in het antwoord voorkomen. Best efficiënt!

Gerelateerde resources direct meesturen

In het voorbeeld dat we net zagen, stuurt de server alle relaties mee in het antwoord. Dit wil je niet altijd, omdat dit het antwoord groter maakt (meer data). Precies om die reden geeft de specificatie je de mogelijkheid om te definiëren welke relaties je van de server terugkrijgt. Wil je bijvoorbeeld alléén de `author`-relatie van de server ontvangen, dan geef je dit aan in de `include` request-parameter.

GET /articles/1?include=author HTTP/1.1
Accept: application/vnd.api+json

Je kunt meerdere relaties opgeven door ze komma-gescheiden mee te sturen (`,`). Dit gaat verder dan directe relaties. Stel, een atikel bevat `comments` waarvan je ook de `author` relatie van wilt ontvangen. Dan geef je gewoon `comments.author` mee in de include-parameter.

GET /articles/1?include=author,comments.author HTTP/1.1
Accept: application/vnd.api+json

Dankzij deze flexibiliteit tweak je snel en simpel je resultaten, zodat je exact de data krijgt die je hebben wilt. 

Sparse fieldsets

Bij het gebruik van compound documents kunnen de requests groot worden, zeker wanneer je relaties óók veel data bevatten. Vaak heb je niet alle API-velden nodig, maar wil je – bijvoorbeeld – alleen de naam van de auteur ontvangen. Geen probleem: JSON API voorziet hierin met het concept sparce fieldsets. Met de `fields` request-parameter specificeer je welke velden de server moet versturen. Het format hiervoor is `fields[TYPE]`. Zo stel je per resource eenvoudig in welke velden je nodig hebt.

Onderstaand verzoek toont de `title` en `body` van de `articles`-resource en de `name` van de `people`-resource.

GET /articles?include=author&fields[articles]=title,body&fields[people]=name HTTP/1.1
Accept: application/vnd.api+json

Andere features

Servers kunnen desgewenst nog meer features implementeren, zoals sorteren, pagineren en filteren.

  • Sorteren

Als de server dit ondersteunt, kun je de data die je ontvangt sorteren met de `sort`-parameter. Standaard gebeurt dit oplopend. Wil je aflopend sorteren, zet dan een `-` voor het veld.

GET /articles?sort=-created,title HTTP/1.1
Accept: application/vnd.api+json

  • Pagineren

Ondersteuning bij paginering verloopt binnen de attributen `meta` en `links`. Hierin wordt informatie meegestuurd over de paginering. De server mag zelf de implementatie bepalen voor de paginering; dit kan dus per server uiteenlopen. De verschillen zitten ‘m dan in hoe de paginering wordt aangestuurd: door een offset te gebruiken bijvoorbeeld, of direct via paginanummers. Een voorbeeld implementatie gebruikt het `links`-attribuut om aan te geven waar andere pagina's kunnen worden opgehaald. En een `meta`-tag om over de hoeveelheden te informeren.

{
  links: {
    first: "http://example.com/articles?page=1",
    last: "http://example.com/articles?page=262",
    prev: "http://example.com/articles?page=261",
    next: null
  },
  meta: {
    current_page: 262,
    from: 3916,
    last_page: 262,
    per_page: 15,
    to: 3924,
    total: 3924
  }
}

  • Filteren

De specificatie is erg open over filteren. Er is alleen vastgesteld dat het met de `filter`-parameter moet gebeuren. Hoe de server het filteren afhandelt, valt buiten de specificatie.

Aanmaken, updaten en verwijderen van resources

Begrijp je eenmaal de structuur van JSON API-documenten, dan wordt data aanmaken en updaten vrij simpel. Voor verzoeken geldt de http-standaard om de gewenste acties uit te voeren: `GET` voor ophalen, `POST` voor aanmaken, `PATCH` voor (gedeeltelijke) updates en `DELETE` om resources te verwijderen.

Resources aanmaken

Hieronder zie je hoe een nieuwe foto aanmaakt. Opvallend: in het verzoek stuur je meteen de `photographer`-relatie mee.

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" }
      }
    }
  }
}

Resources updaten

Wanneer je resources wilt updaten, worden alleen de meegestuurde velden geüpdatet. Eigenschappen die je niet meestuurt, worden niet aangeraakt. Kijk maar:

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"
    }
  }
}

Relaties updaten

Een relatie kun je op twee manieren updaten: óf je stuurt een relatie in de `PATCH` mee, óf je stuurt de nieuwe data naar het specifieke `relationships`-endpoint van een resource.


PATCH /articles/1/relationships/author HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json

{
  "data": { "type": "people", "id": "12" }
}

Dit verzoek update de 'op een'-relatie van het artikel. Wil je een relatie verwijderen, dan geef je `null` als waarde. Als je een ‘op meer’-relatie wilt updaten, stuur je een array met relaties naar het endpoint. Zo vervang je met de nieuwe set alle data in de relatie.

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" }
  ]
}

Wil je alle koppelingen verwijderen, verstuur dan een lege array.

Resources verwijderen

Hier valt niet zo veel over te vertellen: stuur een `DELETE` naar het endpoint voor de resource.

DELETE /photos/1 HTTP/1.1
Accept: application/vnd.api+json

Meer weten?

Op jsonapi.org staan nog veel meer details van de specificatie {json:api}. Allemaal erg duidelijk beschreven, al hoef je niet alle features te implementeren. Op de site vind je ook een lijst met client en server implementaties, zodat je JSON API snel in je eigen applicatie kunt gebruiken.

Op dit moment wordt er hard gewerkt aan versie 1.1. Goed om te weten: deze wordt 100% backward compatible met de 1.0 versie. De nieuwe specificatie voegt dus alleen features toe.

{json:api} <3

Ik hoop dat je wat hebt opgestoken, en snapt waarom ik deze specificatie zo geweldig vind. Ik gebruik ‘m vooral zo graag vanwege de structuur, consistentie en flexibiliteit. Danzkij deze eigenschappen communiceer je een stuk makkelijker en sneller met een API. Je weet precies wat je (terug)krijgt en kunt snel bij de data die je nodig hebt, zonder talloze verzoeken te doen.

Precies daarom werken we hard aan een Laravel package om {json:api} resources naar classes te mappen. En aan een package om een {json:api} server op te zetten, op basis van je Eloquent models. Deze packages gebruiken we in projecten om sneller en efficiënter te werken.

Binnenkort weer een blog over hoe je JSON API in je eigen applicatie gebruikt. Tot dan!