Encore un article sur les bases d'une API REST !

Cet article à été écrit lors d’un live de la chaîne Twitch d’Ippon le 08 Janvier 2021, avec l'aide du chat, merci à eux !

Les APIs REST sont maintenant la norme pour l'exposition de WebServices. On trouve énormément de littérature sur le sujet sur le net, du coup, je me suis dit que j'allais encore écrire un truc pour parler de ce même sujet !

Ce qui m'intéresse ici, c'est d'apporter des éléments aux développeurs pour obtenir des APIs :

  • Bénéficiant des avantages de REST ;
  • Stateless (sans états) ;
  • Simples à comprendre et à utiliser ;
  • Facilement maintenables ;
  • Pérennes.

Ce qui nous intéresse ici c'est donc l'exposition d'API over HTTP.

Over HTTP

Nos APIs vont être transportées en utilisant HTTP(s), nous devons donc respecter les normes de ce protocole.

Idempotence

L'idempotence est une propriété d'un système stipulant que si on fait plusieurs fois la même opération, on doit obtenir le même résultat. La RFC HTTP définit des règles pour l'idempotence :

"Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request. The methods GET, HEAD, PUT and DELETE share this property."

Si on veut respecter cette propriété nous n'avons pas le choix : parmi les verbes utilisés régulièrement, seuls POST et PATCH peuvent ne pas être idempotents. Cela va avoir un impact direct sur le design de nos APIs !

Nous savons maintenant qu'il faut utiliser à bon escient les différents verbes HTTP pour faire nos APIs, ceux que l'on va utiliser le plus souvent sont :

  • POST : pour les créations et les actions ;
  • GET : pour les récupérations d'informations ;
  • PUT : pour les modifications ;
  • DELETE : pour les suppressions.

Status

Les statuts de retour sont une part importante du protocole HTTP, là aussi, nous devons nous plier à ces règles. Il n'est pas forcément nécessaire d'être extrêmement strict sur les statuts précis mais respecter les familles (2XX, 3XX, 4XX et 5XX) est une nécessité.

Certes, un des objectifs de vie de tout développeur est de faire, au moins une fois, une API renvoyant un 418 mais ça, c'est une autre histoire :D.

Un moyen efficace de différencier les 4XX (erreurs utilisateur) et les 5XX (erreurs serveur) est de mettre en place une gestion d'exceptions faisant cette catégorisation.

Headers

Les headers sont une part importante de HTTP, même s'ils ne nous intéressent pas tous dans le cadre du design d'APIs, ceux permettant la négociation de contenu sont tout particulièrement importants.

Ce sont ces headers qui permettront à nos WebServices de choisir :

  • Le format du payload (JSON, PDF, ...) ;
  • La version du payload ;
  • La langue à utiliser pour le retour.

Comment faire ?

Dans le monde Java nous avons maintenant une chance à ne pas négliger : Spring Boot nous aide énormément à respecter les règles de HTTP. Nous allons donc le laisser nous aider au maximum !

Il nous incombe toutefois d'exposer des endpoints cohérents. Pour ce faire, le moyen le plus efficace, à mon sens, est d'exposer des collections de ressources REST. Attention cependant : une ressource REST n'est pas une exposition de notre persistance ! On cherche ici à exprimer les opérations Métier portées par nos APIs.

Le fait d'exposer des opérations Métier, et non pas notre persistance, depuis nos API REST, peut paraître étrange. Je ne considère ici que le cas d'APIs exposant des opérations Métier. Des outils comme Spring Data REST, ou JHipster trouveront toute leur utilité pour la création d'APIs exposant des CRUDs anémiques.

Voici, par exemple, ce que l'on peut avoir comme ressources REST pour une API avec des clients (customers) ayant des factures (invoices) :

POST   /customers
GET    /customers
GET    /customers/{customer-id}
PUT    /customers/{customer-id}
POST   /customers/{customer-id}/invoices
GET    /customers/{customer-id}/invoices
GET    /customers/{customer-id}/invoices/{invoice-id}
DELETE /customers/{customer-id}/invoices/{invoice-id}

Avec cette représentation on traite simplement beaucoup des contraintes de notre API :

  • On exprime clairement les opérations métier, ce qui assure une part importante de la pérennité de l'API ;
  • On peut respecter l'idempotence ;
  • On peut router simplement les requêtes vers des machines dédiées (pour distribuer nos Bounded Contexts) ;
  • La négociation de contenu peut nous donner simplement des factures sous forme de JSON (pour un affichage dans une page Web) ou sous forme de PDF (qui est le standard pour une facture).

Si on avait fait GET /createJsonCustomer (comme on le voit parfois) nous n'aurions absolument pas respecté ces contraintes !

Un point d'attention cependant pour la partie DELETE. Par défaut, hibernate renvoie une exception si on delete un entrée qui n'existe pas : notre opération ne sera donc pas idempotente. Il faut vérifier la présence de l'entrée avec un exists, puis faire le delete.

Dans notre API, la récupération des customers et des invoices remonte forcément toutes les entrées : ce n'est pas ce que l'on ferait sur une vraie API qui a besoin de pagination et de recherche. Pour ce faire, nous allons utiliser les query strings pour ajouter ces paramètres :

GET /customers?terms=XXX&page=0&pageSize=20
GET /customers/{customer-id}/invoices?page=0&pageSize=20&after=2020-12-12

Manipuler des ressources REST peut cependant avoir une limite : le cas du traitement d'actions métier. Ces opérations sont rarement idempotentes. Si on prend en compte le fait que PATCH n'est pas toujours supporté par les équipements réseau, il ne nous reste qu'une option pour ce cas : POST avec un verbe dans l'URL qui décrit notre action. Par exemple, pour annuler (cancel) une facture (invoice) on aurait donc :

POST /customers/{customer-id}/invoices/{invoice-id}/cancel
Dans ce cas, le Métier considère que l'annulation d'une facture n'est absolument pas la même chose que sa suppression (qui viendra plus tard).

Level of REST & RestFul & HATEOAS

Cet article décrit une manière simple de désigner des APIs REST ayant un niveau de REST relativement élevé. Il est cependant théoriquement possible d'aller plus loin, notamment avec HATEOAS qui permet la découverte dynamique des APIs.

De mon point de vue, l'outillage pour faire du HATEOAS n'est pas mature. Sa mise en place aura donc un coût important (voire très important) et je ne trouve pas que les apports justifient cette surcharge.

Au quotidien je design et je consomme des APIs faites en respectant cette règle de collections de ressources qui répond très bien à mes attentes. Le chat, pendant l'écriture de cet article, a aussi validé cette approche.