Au top du développement d’API avec HATEOAS !

Vous avez remarqué qu’aujourd’hui, notre bon vieux World Wide Web dispose d’une multitude de services, permettant aux honorables développeurs comme nous de profiter des fonctionnalités proposées par différentes plateformes comme Facebook, Twitter, etc… Si  on veut développer une application Web proposant le calendrier complet de Ligue 1, avec les effectifs, les scores, le classement, il est inutile de gérer toutes ces données, vu que c’est déjà fait ailleurs !

Que faire ? Trouver la bonne plateforme proposant une API publique (gratuite d’utilisation ou non), coder les requêtes HTTP nécessaires à la récupération des données, présenter tout cela sous une jolie forme au client… Et voilà notre application faite ! Et aucune grosse base de données à gérer soi-même.

Mais ça, c’est si on est seulement consommateur de ces données. Mais si on a les données, et que l’on veut proposer une API afin que différents consommateurs profitent de nos informations, quelle architecture pouvons-nous mettre en place ?

Le besoin client : tous pour un, un pour tous !

Dans ma mission actuelle, le nombre d’applications différentes développées est relativement conséquent. Certaines étaient des applications lourdes, d’autres des applications web… chaque équipe était dans son projet, et développait comme bon lui semblait (même si certaines données étaient communes à tous).

Le client a donc estimé que tout cela n’était pas dans son intérêt, et qu’une stratégie d’uniformisation de ses applications pouvait apporter une réelle plus-value vis-à-vis de ses clients. Ne serait-il pas mieux d’avoir des applications à la charte graphique semblable ? De pouvoir accéder à toutes leurs applications via un seul point d’entrée ? De n’avoir à s’authentifier qu’une seule fois pour accéder à tout le panel des fonctionnalités offertes ?

C’est là qu’est née l’idée d’un Framework, développé entièrement en interne, et proposant les outils nécessaires afin de réaliser ce paradis du développeur. Le but ? Avoir un socle commun pour toutes les équipes afin de proposer des applications homogènes, pour lesquelles les développeurs n’avaient pas (ou peu) à s’occuper de la partie présentation HTML/CSS.

La réflexion: be the best, use REST !

Il n’est pas question dans cet article de s’attarder sur la partie présentation (HTML, CSS) du Framework, mais plutôt de présenter la façon dont nous avons pu créer des applications entières avec seulement des appels à des API, sans que le front-end n’ait à gérer quoi que ce soit niveau métier.

Toute la quintessence d’une API est de pouvoir fournir une ressource en ne se basant que sur la requête reçue. Autrement dit, la bonne pratique est de créer des API stateless. Et pour cela, quoi de mieux que l’architecture REST ?

Je ne vais pas entrer dans les détails de l’architecture REST, étant donné que c’est une pratique qui est maintenant relativement courante. Cependant, pour celles et ceux qui seraient en congé sabbatique depuis… environ 15 ans, il serait juste bon de rappeler que REST préconise d’utiliser les URL et les verbes HTTP pour définir la ressource et/ou l’action attendue(s) par l’appel.

Un petit exemple quand même ? Bon d’accord… Reprenons l’exemple de la Ligue 1. Si je veux obtenir la liste des clubs du championnat, une requête fidèle à la philosophie REST serait :

GET -> http://monsite-ligue1.fr/api/clubs/

La requête se lit presque d’elle-même : je veux obtenir (GET) la liste des clubs. Et notre API retournera alors une ressource qui peut ressembler à cela (si nous choisissions le JSON comme type de retour) :

[
  {
    id: 69,
    name: "Olympique lyonnais",
    color: "white"
  },
  {
    id: 57,
    name: "Football Club de Metz",
    color: "purple"
  }
]

On a bien la liste (non exhaustive pour l’exemple bien sûr) des clubs de Ligue 1, avec un minimum d’info. Côté serveur, le travail est fait ! Il a reçu une demande, il a répondu, et il continue sa vie comme si rien ne s’était passé. Côté client maintenant : j’ai reçu ma ressource, je l’affiche à l’écran… et l’utilisateur me signale qu’il veut en savoir plus sur l’Olympique lyonnais (club pris totalement au hasard, évidemment…). Dans ce cas, je peux faire cette requête :

GET -> http://monsite-ligue1.fr/api/clubs/69

Cette fois-ci, le serveur comprend qu’il doit chercher dans ses clubs, celui dont l’id est 69. Dans ce cas-là, il renvoie un autre type de ressource, qui pourrait être semblable à ceci :

{
  id: 69,
  name: "Olympique lyonnais",
  color: "white",
  creationDate: "01/07/1950",
  ville: "Lyon",
  //et tout un tas d'autres informations
}

C’est intéressant, mais notre site ne serait pas tellement intéressant si nous ne faisions que demander des informations au serveur. Et c’est là que les verbes HTTP interviennent ! Nous disposons en plus des verbes POST, PUT et DELETE afin de faire comprendre au serveur ce que nous souhaitons faire. Ces verbes permettent de gérer les actions CRUD, sans pour autant changer la sémantique de l’URL. Bien sur, il en existe d’autres (HEAD, PATCH, TRACE, OPTIONS…) permettant d’autres actions, mais ceux cités juste avant nous permettent déjà de faire vivre pleinement l’application.

Ah ! Dernière petite chose ! L’autre intérêt de REST est aussi pour le serveur de pouvoir retourner des erreurs différentes selon les demandes du client ! Dans toutes les requêtes que nous avons faites précédemment, le serveur a pu tout traiter sans problème, et a donc renvoyé le code 200 à chaque fois. Maintenant, revenons un peu en arrière, et imaginons que je veux supprimer le club avec l’id 38. Celui-ci n’étant pas dans la liste, le serveur ne peut trouver la ressource demandée, et renvoie donc le fameux et non moins terrifiant code 404. Si maintenant, je change le verbe DELETE par un POST, pour créer ce club, mais malheureusement, j’oublie de fournir son nom, alors que c’est un champ obligatoire. L’URL n’a pas changée, seul le verbe est différent, mais dans ce dernier cas, l’erreur retournée sera plus vraisemblablement un code 400 (la syntaxe de la requête est erronée).

Ce rappel était surtout utile pour se remémorer comment les API comprennent ce que demande le client, avec seulement le contenu de la requête HTTP. Maintenant, tout cela est bien beau, mais cela ne nous dit pas comment créer un site entièrement navigable, avec seulement des appels à notre (ou nos) APIs, et avec un client qui n’a aucune connaissance du métier. Car dans les exemples donnés précédemment, nous sommes partis du principe que le client connaissait déjà l’URL à appeler pour avoir la liste des clubs, et qu’il connaissait par conséquent le type de ressource qu’il allait recevoir en retour, afin de l’afficher correctement.

Et bien il y a un moyen pour le client de s’affranchir de cette connaissance, et c’est maintenant que nous entrons dans la magie d’HATEOAS !

Soyez un as, utilisez HATEOAS !

Si on résume ce que l’on vient de voir, finalement, nous ne faisons que des demandes de ressources à un serveur, que nous affichons à l’utilisateur, qui interagit avec, ce qui constitue le cycle de vie de notre application. Mais du coup, ne serait-il pas merveilleux que le serveur nous dise lui-même ce que l’on peut obtenir comme ressource, à partir de la requête courante?

C’est le principe d’HATEOAS (Hypermedia As The Engine Of Application State). Imaginons que je viens de demander ma liste des clubs, et le client l’affiche à l’utilisateur. Que puis-je faire avec cette liste ? Je peux ajouter un club par exemple. Et que puis-je faire pour chaque club déjà affiché ? Je peux voir ses détails. Maintenant, réfléchissons un peu (pas trop)… finalement, tout ce que l’on peut faire à partir de cette liste, ce ne sont que des actions que l’on peut traduire en requêtes HTTP via REST et les verbes !

Par conséquent, pourquoi le serveur ne fournirait-il pas au client tous les liens possibles pour effectuer ses actions ? Ainsi, le client n’a plus à connaitre les URL à appeler, il a juste à utiliser celles qu’on lui fournit. Mais un exemple parlant mieux que mille mots, voyons à quoi pourrait ressembler la ressource que l’on recevrait du serveur à la demande de la liste des clubs.

GET -> http://monsite-ligue1.fr/api/clubs/

{
  data: [
    {
      data: {
        id: 69,
        name: "Olympique lyonnais",
        color: "white"
      },
      links: [
        {
          rel: "seeDetails",
          url: "http://monsite-ligue1.fr/api/clubs/69",
          method: "GET"
        }
      ]
    },
    {
      data: {
        id: 57,
        name: "Football Club de Metz",
        color: "purple"
      },
      links: [
        {
          rel: "seeDetails",
          url: "http://monsite-ligue1.fr/api/clubs/57",
          method: "GET"
        }
      ]
    }
  ],
  links: [
    {
      rel: "add",
      url: "http://monsite-ligue1.fr/api/clubs/",
      method: "POST"
    }
  ]
}

Wow, c’est bien plus gros comme ressource ! Effectivement… mais la puissance en est décuplée ! On remarque tout d’abord que maintenant, chaque ressource est divisée en deux parties : les données (data) et les liens (links). Les données sont ce que le client va afficher à l’écran, et les liens décrivent ce que l’utilisateur peut demander à partir de cette ressource. Ainsi, à partir de la liste des clubs, nous voyons qu’il est possible de faire une action, dont le nom est « add » et qui décrit l’URL et le verbe à utiliser pour l’effectuer. Le client aura donc juste à afficher un formulaire que l’utilisateur devra remplir, puis l’envoyer à cette URL avec ce verbe pour effectuer l’action d’ajout d’un club. De la même manière, chaque club retourné possède un lien « seeDetails » permettant de voir ses détails.

Pratique n’est-ce pas ? Le client n’a plus à connaître les URL pour que l’utilisateur puisse naviguer sur le site ! Autre avantage : le serveur peut fournir certains liens ou non, suivant les droits de l’utilisateur connecté. Le client n’a donc pas besoin de savoir qui est connecté pour savoir s’il a le droit de faire telle ou telle action.

Ah mais… attendez… Il n’y a pas quelque chose qui cloche ? D’accord le client n’a plus à connaître les URL, et n’a pas à connaître les droits de l’utilisateur connecté. Mais pour ajouter un club, il faut bien savoir quels champs sont à proposer pour le formulaire de création !

Et bien c’est vrai, et c’est là la grande limite d’HATEOAS. HATEOAS n’est qu’une suggestion de bonnes pratiques afin de construire un site totalement navigable uniquement via les URL. D’ailleurs, je suis allé un peu vite en ajoutant directement les verbes HTTP dans les liens de la ressource : normalement, HATEOAS ne préconise rien à ce niveau-là. On se retrouve donc avec un principe qui part d’une bonne intention et dont on imagine très bien les avantages qu’il peut apporter, mais nous n’avons toujours pas vraiment découplé notre partie IHM de notre partie serveur, vu que le client doit toujours connaître les ressources qu’elle gère, afin de créer des formulaires adaptés, ou même des présentations simples de données.

Notons tout de même que cette pratique permet à notre API de respecter le plus haut niveau (3) du modèle de maturité de Richardson, qui évalue une application selon son respect des contraintes REST. (Pour plus d’informations sur ce modèle : Présentation du modèle de maturité de Richardson)

Malgré tout, cela n’était pas suffisant dans le cadre de la mission que j’effectuais. Il a donc fallu agrémenter cette convention, afin de séparer complètement l’IHM du serveur, et donc de dispenser le client de tout savoir métier.

L’arme fatale : les contrats !

Que veut-on dire par contrat ? C’est en fait la description d’une ressource que le client est susceptible de devoir gérer pendant la durée de vie de l’application.

Imaginons que je veux créer un club, qui viendra s’ajouter dans la liste. Cette dernière me fournit un lien « Add » qui décrit donc l’URL à appeler pour créer mon club. Oui, mais voilà… quelles sont les informations nécessaires à fournir au serveur afin de créer ce club ? Il y a deux solutions : soit le client connait ce qu’il doit envoyer, soit le serveur lui fournit ce dont il a besoin.

Croyez-le ou non, nous allons préférer la deuxième méthode ! Et cette description de la ressource attendue par le serveur est notre fameux contrat. Et cette méthode va nous apporter tellement de bénéfices, qu’il vaudrait mieux commencer par une illustration concrète, avant d’en analyser les avantages.

Voici un exemple de ce que pourrait être le contrat du formulaire d’ajout de club :

{
  name: "newClubForm",
  type: "form",
  label:  "Ajouter un club",
  fields: [
    {
      id: "name",
      label: "Nom du club",
      type: "Text"
    },
    {
      id: "dateCreation",
      label: "Date de fondation",
      type: "Date"
    }
  ]
}

Cela fait beaucoup d’informations d’un coup ! Faisons une version littéraire de ce contrat : il existe une ressource nommée « newClubForm », qui est un formulaire, et qui peut avoir un titre « Ajouter un club ». Ce formulaire présente 2 champs :

  • Un champ identifié « name », qui demande du texte, et titré « Nom du club »
  • Un champ identifié « dateCreation », qui demande une date, et titré « Date de fondation »

C’est tout ce que ce contrat nous annonce. Maintenant, il suffit de faire le lien avec notre lien « add » présenté dans la liste des clubs, comme cela :

links: [
    {
      rel: "add",
      url: "http://monsite-ligue1.fr/api/clubs/",
      method: "POST",
      resourceType: "newClubForm"
    }
  ]

De cette façon, lorsque l’utilisateur fait appel au lien « add », le client sait qu’il doit afficher une ressource de type « newClubForm ». Par conséquence, à aucun moment, le client n’a eu à connaitre les ressources gérées jusque-là, de l’affichage de la liste jusqu’à l’ajout d’un nouveau club !

En effet, si nous résumons, voici ce qui s’est passé :

  • Le serveur a fourni au client une liste de contrats, décrivant les ressources susceptibles d’être utilisées lors de l’utilisation de l’application. Une fois ceci fait, l’application démarre
  • Le client charge la liste des clubs, en utilisant le contrat (par exemple « clubCollection ») qui définit le lien à utiliser pour charger ses données. Une fois affichée, l’utilisateur clique sur le bouton « Ajouter » qui utilise le lien nommé « add ». Le client voit que cette action nécessite de fournir au serveur une ressource de type « newClubForm », et affiche donc un formulaire avec tous les champs décrits dans le contrat du même nom.
  • L’utilisateur remplit les champs, et soumet le formulaire. Le client fait donc l’appel au serveur tel que décrit par le lien « add ». Une fois que le serveur a répondu, la page retourne sur la liste, qui se rafraîchit (à l’aide du même lien utilisé au premier affichage). Nous avons maintenant notre liste agrémentée de notre nouveau club fraichement créé.

Puissant n’est-ce pas ? Au final, le client n’a besoin de savoir que faire deux choses :

  • Afficher une liste
  • Afficher un formulaire

Et c’est tout ! À aucun moment il ne connait exactement la ressource à afficher. Supposons par exemple, que notre site évolue, et s’intéresse aux stades. Nous aurons donc un nouveau formulaire nommé « newStadiumForm », mais qui sera toujours de type « form ». Du coup, le code client pourra gérer l’affichage de ce formulaire exactement comme celui du club. Aucun code supplémentaire ! Pour information, dans notre cas, nous créons des widgets jQuery afin de gérer cela.

Avantages, (petits) inconvénients et évolutions

Les avantages sont appréciables :

  • Comme nous le voulions depuis le départ, côté client, nous n’avons plus aucune connaissance métier. Tout est géré et décrit par le serveur.
  • Économie de code côté client : tous les formulaires par exemple, peuvent être traités avec un seul et même widget.
  • La traduction est gérée côté serveur ! En effet, grâce aux contrats qui définissent ce qu’il y a à afficher, avec les libellés, le client n’a pas à s’occuper de cela.

Les inconvénients sont donc réduits :

  • Il faut charger les contrats à l’ouverture de l’application. Selon le volume de contrats, cela peut prendre un peu de temps.
  • Le client doit tout de même connaître le point d’entrée, c’est-à-dire le contrat dont les données sont à charger en premier.
  • Il y a toujours des cas spéciaux ou un widget générique ne peut convenir. Par exemple, notre widget de formulaire convient très bien pour ceux avec peu de champs. Si ce nombre est conséquent, il est sans doute préférable d’avoir un affichage personnalisé, et donc une surcharge du widget de base est indispensable.

Quelques évolutions sont bien évidemment possibles. Voici une liste non exhaustive de ce que nous avons fait pour améliorer ce système, ou de ce qui pourrait être fait :

  • Un générateur de contrats peut être créé côté serveur. Il suffit de s’appuyer sur les propriétés d’une classe (avec l’aide de quelques attributs) pour générer les contrats correspondants.
  • Centraliser les liens directement dans les contrats (et gérer les variables à insérer dans les URL grâce à un moteur de template – Mustache.js dans notre cas)

 

Retour sur expérience

Lorsque nous avons mis en place ce système, beaucoup de nos collègues étaient dubitatifs. Dans l’ensemble, ce sont surtout des anciens de l’entreprise, qui étaient très attachés à leurs applications lourdes, et qui du coup appréhendaient un peu l’arrivée du JavaScript dans leur vie. Mais comme nous l’avons démontré dans cet article, la partie cliente JavaScript est très réduite, du fait que nous avons fourni les widgets capables de gérer les données génériques (liste, formulaire, affichage d’entités…).

Nous avons donc confié la phase de « beta-test » à certains développeurs motivés pour implémenter ce concept dans leurs applis (nouvelles ou remplacement d’existant). Il faut avouer que durant les premiers mois, la prise en main a été compliquée. Ce n’est pas au niveau du code que cela bloquait, mais plutôt au niveau de la compréhension du concept. De plus, nous avons fourni les widgets de base, ainsi que le générateur de contrats. Par conséquent, nous avons dû définir un bon nombre d’attributs qu’il était nécessaire de connaître afin que les contrats soit générés proprement.

Néanmoins, une fois le système pris en main, tout le monde s’accorde à dire que la création d’une application avec ce que nous appelons le Framework (qui regroupe tout le nécessaire coté back-end, ainsi que de nombreux widgets jQuery) est d’une simplicité réellement appréciable, tant au niveau du temps de réalisation que de la qualité d’implémentation. Nous avons même réussi à convaincre les plus réfractaires du bien fondé de notre système, et je sais que vous en connaissez tous dans vos missions, et que vous savez combien c’est agréable de les faire changer d’avis !

Laisser un commentaire

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