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 :

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

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 :

  • Listing des commandes :
    GET http://api.mondomaine.org/orders
    Les commandes sont retournées par 20, triées par date de création. Pour naviguer dans les différentes pages, utiliser l’URL, avec une valeur entière positive pour “page” :
    GET http://api.mondomaine.org/orders{?page}
  • Accès à une commande particulière :
    GET http://api.mondomaine.org/orders/{uid}
    où uid est l’identifiant de la commande concernée, nommé “uid” dans la représentation JSON
  • Accès au client ayant passé la commande :
    GET http://api.mondomaine.org/orders/{uid}/client
    où uid est l’identifiant de la commande concernée

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 <a> et </a> é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/

Tweet about this on TwitterShare on FacebookGoogle+Share on LinkedIn

3 réflexions au sujet de « Hypermedia API »

  1. Bonjour,
    Merci pour ce petit tuto.

    j’ai une question au niveau performance/optimisation et bonne architecture REST.

    Imaginons que tu souhaites dans ton app mobile afficher la liste de tes commandes et que dans cette liste tu veux aussi afficher le nom/prénom du client par commande.

    Donc tu fais une requête HTTP pour avoir toutes les commandes avec http://api.mondomaine.org/orders et tu fais N requêtes sur http://api.mondomaine.org/orders/N/client soit 20 requêtes HTTP pour une page.

    Sauf que, au niveau de ma base coté serveur il est assez simple d’avoir toutes les infos en une seul requête SQL et qu’il est facile de retourner toutes les données lié en une seul fois également. Enfin, du coté de mon app mobile, je ne fais qu’une seul requête HTTP au lieu de 21…

    Y a t’il une bonne pratique dans la conception de son API en REST pour faire ça ?

    Merci

    1. Il n’y a pas de réponse unique… mais creusons un peu le sujet.
      Chacune des approches va avoir ses propres avantages et inconvénients. Il s’agit de faire un choix dans un contexte précis.

      On parle ici d’application mobile consommant une API. Qui dit mobile dit contraintes spécifiques, dont la connectivité (latence, bande passante…).
      Dans ce cas, une seule requête, optimisée, peut s’entendre. Cette requête (exemple: http://api.mondomaine.org/orders/) recevra une réponse représentant une collection de représentations simplifiées des commandes. Ce ne sera certainement pas la même représentation que celle qui serait retournée sur une requête spécifique à une commande (http://api.mondomaine.org/orders/{id}), laquelle sera vraisemblablement plus détaillée.

      Quels sont les inconvénients de cette approche (embedded) par rapport à une réponse ‘stricte’ (linked) ?

      – dès lors où des ressources sont embarquées, la gestion du cache doit être judicieusement réalisée car le risque de données périmées devient fort. Ou bien le cache n’est pas activé sur ces ressources, ou bien il est acceptable de retourner une réponse partiellement périmée.
      – l’API se spécialise fortement pour un client donné. Le besoin mobile n’est peut être pas celui d’un autre client.

      Relativisons maintenant les N+1 requêtes par rapport à 1 requête. Lorsqu’un mobile (ou n’importe quel client) doit afficher une liste avec des images (représentant des produits par exemple), il est rare que les ressources images soient embarquées dans la réponse sous forme de Data URI. Le client doit donc faire les N requêtes supplémentaires, et souvent personne ne s’en indigne… Mais ces N requêtes peuvent bénéficier du cache HTTP : d’une part le cache privé du client, ce dernier ne fera donc pas ces requêtes à chaque affichage, mais aussi des caches partagés sur le chemin de la requête (qui permettront de ne pas solliciter toujours le serveur).

      Il n’existe pas de bonne pratique pour ce cas, juste des choix pragmatiques. Utiliser des ressources embarquées est bien sûr recevable, on le voit par exemple dans la documentation du format HAL (voir exemple dans le paragraphe “application/hal+json”). Ceci ne doit par contre pas se faire au détriment de la navigation entre les ressources et donc des relations hypermedia qui lient les ressources entre elles. La présence des deux (données embarquées + relation hypermédia) permettra à un client ‘lazy’ de se contenter des données embarquées là où un client plus sophistiqué (et moins contraint) pourra parcourir les ressources associées.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *


*