Hypermedia API

HypertextDévelopper une API Web est devenu quelque chose de fréquent dans les projets. Les motivations sont multiples : répondre à des besoins internes d’exposition des données, souhait de faire naître un écosystème autour de ses données, créer une nouvelle opportunité de business… Dans tous les cas, on attend de l’API qu’elle soit utilisée par beaucoup de clients, d’autant plus si elle est monétisée…

Plus le nombre d’utilisateurs de l’API croît et plus les modifications sur cette dernière peuvent devenir sensibles, en particulier si les bonnes décisions n’ont pas été prises dès le début. Étudions un aspect qui favorise l’évolutivité d’une API sans pénaliser l’écosystème existant.

Hypermedia, relations, … on en entend de plus en plus parler. Mais l’intérêt n’est peut être pas encore évident dans une API. Pourtant, nous allons voir que cette mise en oeuvre peut être porteuse d’un certain nombre d’avantages. Découvrons donc aujourd’hui l’utilisation des relations explicites dans les API (qu’on pourrait ainsi qualifier d’Hypermedia API).

Rappel :

Lorsqu’il s’agit de trouver des exemples de construction d’API, bien souvent il s’agit d’illustrations de cas de ce type : accès en lecture/écriture à des entités persistées (un bon vieux CRUD) avec sérialisation de ces entités dans la couche d’exposition, souvent en JSON.

Considérons un de ces cas simplistes : une API pour gérer une liste de commandes (imaginons pour une chaîne de restauration rapide). Une “commande” va désigner une ressource comportant quelques attributs (timestamp, référence à un menu, prix total, statut, …) et sera persistée dans un système quelconque (bien souvent une base de données).

Bien souvent, l’approche proposée par les tutoriaux sera d’ajouter quelques annotations Jackson à l’entité JPA afin d’en permettre la sérialisation/désérialisation en JSON. La création d’un contrôleur Spring (avec l’annotation @RestController [1] pour ceux qui ont déjà adopté Spring 4, ou un plus “simple” @Controller pour les autres) permettra d’exposer l’API.

Il est assez simple ainsi de pouvoir servir des requêtes HTTP, qui dans l’exemple courant pourraient être :

Et qui retourneraient, pour celles qui retournent des représentations, tantôt des collections d’objets “commande”, ou l’objet “commande” lui même, sous des formes qui pourraient ressembler à ceci :

Une liste :

[
    {
        "uid": 1,
        "price": 6.4,
        "name": "Menu A",
        "quantity": 1,
        "created": "2014-03-26T11:55:06.142+0000"
    },
    {
        "uid": 2,
        "price": 14.6,
        "name": "Menu B",
        "quantity": 2,
        "created": "2014-04-01T13:27:07.628+0000"
    }
]```

et une commande seule :

{
"uid": 2,
"price": 14.6,
"name": "Menu B",
"quantity": 2,
"created": "2014-04-01T13:27:07.628+0000"
}```

Note : la représentation d’une commande ici est très loin d’être parfaite 😉

La vraie vie maintenant :

Dans la vraie vie, plusieurs subtilités vont s’immiscer dans le scénario précédent…

Le listing des commandes va probablement croître suffisamment pour que la pagination deviennent obligatoire…

Une commande va pouvoir être créée bien sûr, mais aussi modifiée, annulée, réglée, préparée, livrée… Bref, l’état va évoluer et il va falloir proposer des moyens pour ce faire.

Une commande va être liée vraisemblablement à un client, à un paiement. Elle contiendra un certain nombre de plats, qui eux-mêmes pourraient avoir une description détaillée…

Pour exprimer ces cas de figure, l’approche précédente sera probablement accompagnée d’une documentation décrivant les différentes URL disponibles.

Exemple de ce que pourrait être une documentation associée à cette API :

Une application cliente qui consomme cette API va donc devoir être programmée pour proposer les différentes options de navigation après les avoir construites (en s’aidant de classes utilitaires si le pattern utilisé suit la convention Uri Template [2] comme ici). Pour cela, le développeur du client s’appuiera sur la documentation décrivant les différentes ressources exposées et pour chacune les possibilités offertes, potentiellement contextuelles (ex. : on n’annule pas une commande livrée…).

Une telle approche lie fortement le client à l’API : une partie de la logique est dupliquée, les fragments d’adresses sont hardcodés, et les adresses forgées par le client (risque d’erreur)…

Alors comment exprimer ces possibilités d’une façon qui soit à la fois simple à comprendre par l’humain mais aussi par le client, tout en permettant à l’API d’évoluer plus tard sans casser l’écosystème qui aurait pu se construire autour d’elle ?

Et bien en introduisant la notion de relations dans les représentations retournées, sous forme de méta-données accompagnant les données directement en lien avec la ressource présentée.

Imaginons en effet qu’un GET sur une “commande” retourne maintenant ceci :

GET http://api.mondomaine.org/orders/1

{
    "data": {
        "uid": "2",
        "price": 14.6,
        "name": "Menu B",
        "quantity": 2,
        "created": "2014-04-01T13:27:07.628+0000"
    },
    "metadata": {
        "links": [
            {
                "href": "http://api.mondomaine.org/orders/2",
                "rel": "self",
                "desc": "The current order representation"
            },
            {
                "href": "http://api.mondomaine.org/orders/2/client",
                "rel": "client",
                "desc": "This order's client representation"
            },
            {
                "href": "http://api.mondomaine.org/orders/2/payment",
                "rel": "payment",
                "desc": "The payment information, if any"
            }
        ]
    }
}

La représentation offre maintenant sous metadata.links les adresses des ressources en relation, identifiables via un mot clef (exprimé dans “rel”). Le protocole HTTP fournit quant à lui l’ensemble des méthodes permettant de manipuler les ressources. L’API se charge par ailleurs de valider si une modification demandée est bien conforme à l’état courant de la ressource (ce n’est pas parce qu’on dispose de la méthode DELETE qu’il est possible d’en faire usage sur toutes les ressources quel que soit leur état).

Quels sont les avantages à adopter une approche intégrant des liens hypermedia ?

  • Permettre au client de s’abstraire davantage des spécificités de l’API
  • Ne plus imposer au client le besoin de construire une URL selon la règle indiquée dans la documentation
  • Permettre à l’API de modifier sa structure arborescente sans impacter les clients déployés
  • Dans certains cas d’API décrivant des workflows, être en capacité de recomposer le flow sans impacter les clients existants

Et les inconvénients ?

  • La réponse est un peu plus verbeuse (mais une fois compressée, est-ce vraiment toujours un problème?)
  • Développer une telle API sera un peu plus compliqué comparé aux approches simplistes qui ne font que sérialiser l’objet représentant la ressource dans le code
  • Les frameworks intégrant le support de cette notion de liens hypermedia ne sont pas encore nombreux/matures

Pour aller plus loin dans la démarche, détaillons deux aspects en lien direct.

Tout d’abord, le format pour représenter cette ressource. Le premier réflexe pourrait être de créer un format du type de celui présenté au dessus, c’est à dire custom, et de communiquer sur sa structure dans la documentation de l’API. Bien que tout à fait fonctionnel, ce n’est sans doute pas la meilleure option. Il existe en effet des initiatives pour représenter des ressources tout en apportant un support pour les relations hypermedia. Citons parmi celles-ci : Hypertext Application Language (HAL) [3], Siren [4], Collection+JSON [5]… mais aussi Atom [6], HTML…

Ce dernier peut surprendre, car on le réserve plus naturellement aux représentations “visuelles” et non “techniques” (qui seront plus aux formats JSON, XML). Pour autant, HTML dispose d’éléments susceptibles de servir les différents besoins cités plus haut :

  • Structurer les données dans des éléments
  • Présenter des ressources sous forme de liste
  • Représenter les relations (les éléments  et  étant particulièrement adaptés…)

Quel que soit le choix retenu, adopter un format déjà existant, par opposition à créer son propre format, va permettre :

  • Au développeur de l’API - De proposer à ses clients un format.
  • De pouvoir développer son API en s’aidant d’une implémentation dans le langage de son choix pour faciliter la création des représentations. En effet, chaque format dispose souvent de plusieurs implémentations dans différents langages.
  • De partager le même format quelle que soit la ressource représentée, voire même l’API développée.
  • Au client - De pouvoir développer un module client qui ne soit pas forcément spécifique à votre API ou à sa version courante. Il pourra même certainement s’appuyer sur une librairie existante sachant consommer le format proposé. Rappelez-vous ceci : faciliter le travail des clients, c’est maximiser les chances d’adoption de l’API

Le second aspect lié concerne les noms donnés aux relations (l’attribut “rel” bien souvent). Ces noms gagnent à ne pas être tous inventés. En effet, il peut être intéressant de s’appuyer sur des définitions déjà existantes, comme la liste des relations de l’IANA (Internet Assigned Numbers Authority) [7], les noms de relations relatives aux microformats [8], …
Il peut aussi être nécessaire de vouloir exprimer des notions qui n’existent pas dans ces standards. Dans ce cas, la création de ses propres noms de relations est bien sûr possible.
Une bonne pratique peut être de nommer la relation avec une URL, laquelle permettra d’en apprendre plus sur le sens de la relation.

Par exemple :

<link rel=”http://api.mondomaine.org/rels/client” href=”http://api.mondomaine.org/orders/2/client” />

En suivant le lien http://api.mondomaine.org/rels/client, une explication de ce qu’est un client, ce que signifie une relation de ce type, la représentation associée, … sera présentée.

Concluons maintenant sur ces aspects.

Vous devez maintenant avoir une vision différente de ce que peut être une API Web et des possibilités offertes par l’expression explicite des relations entre les ressources exposées. Parmi les principaux gains, il faut retenir un plus fort découplage entre le client de l’API et l’API elle-même. L’API pourra ainsi plus facilement évoluer dans le temps sans rompre la compatibilité avec le parc des clients existants. Par ailleurs, en exploitant les relations retournées par l’API (par opposition à présumer des actions disponibles en se basant sur une version particulière de la documentation de l’API), il sera possible de modifier la cinématique des actions possibles sur le client sans avoir à re-développer ce dernier.

Enfin, consommer des liens décrivant des relations en s’aidant de la sémantique offerte par le protocole et permettant ainsi d’agir sur l’état des ressources, n’est-ce pas ce qu’on définit aussi sous l’acronyme imprononçable, mais que vous avez sûrement tous déjà vu : HATEOAS ?

Stay tuned for the next episode on… Ippon Technologies

[1] http://docs.spring.io/spring-framework/docs/4.0.x/javadoc-api/org/springframework/web/bind/annotation/RestController.html
[2] http://tools.ietf.org/html/rfc6570
[3] http://stateless.co/hal_specification.html
[4] https://github.com/kevinswiber/siren
[5] http://amundsen.com/media-types/collection/format/
[6] http://tools.ietf.org/html/rfc5023
[7] https://www.iana.org/assignments/link-relations/link-relations.xhtml
[8] http://microformats.org/wiki/existing-rel-values

Illustration : https://www.flickr.com/photos/daniel-rehn/