Concevoir des API REST élégantes

Actualité ekino -

Quelques bonnes pratiques et pistes de réflexion

Article paru le :
Concevoir des API REST élégantes
Article paru le :

Chaque début de projet concernant une API REST implique son lot de débats sur le bon design à appliquer. Que ce soit pour le nommage, le format des urls, ou tout autre paramètre, chacun a une opinion tranchée sur le sujet qui diverge parfois avec celle de son voisin.

C’est de ce constat que part Guillaume Laforge dans sa conférence  “Le design d’API REST, un débat sans fin ?” présentée à la Devoxx 2016.

Sans prétendre avoir un modèle parfait applicable tel quel sur tous les projets, Guillaume Laforge nous propose dans cette conférence un état de l’art des bonnes pratiques pour nous aider à concevoir des API REST soignées. Afin de mieux mettre en perspective ces normes et comprendre leurs éventuelles limites, nous verrons également comment ont été traités ces sujets sur un projet concret développé chez ekino.

 

Nommage

La première question à se poser est celle du format des urls que l’on va utiliser. On entendra parfois auprès des développeurs zélés que l’on ne peut utiliser que des noms dans les urls, que les verbes sont à proscrire en toutes circonstances.

En réalité on préférera utiliser des noms mais les verbes peuvent également avoir leur utilité. En effet, les noms désignent des ressources, mais les verbes quant-à-eux prennent tout leur sens lorsqu’on les utilise pour des actions ou des opérations.

Par exemple sur notre projet :

  • Pour récupérer une liste d’utilisateurs : /users
  • Pour rafraîchir le token d’authentification de l’utilisateur connecté : /users/auth/refresh

 

En ce qui concerne la question du singulier ou du pluriel, le standard adopté massivement par les développeurs d’API est le pluriel (ce qui ne vous empêchera pas de trouver des débats passionnés sur l’utilisation du singulier sur certains forums). En utilisant le pluriel on conserve une sémantique correcte tout en ayant un préfixe unique pour toutes les routes traitant d’un même sujet.

Sur notre projet, on aura :

  • Pour récupérer une liste de compagnies : /companies
  • Pour récupérer une compagnie à partir de son identifiant : /companies/{companyId}

 

On peut penser la deuxième route comme “je veux, parmi la liste des compagnies, récupérer l’élément qui a cet identifiant”.

Pour autant, on ne s’est pas complètement interdit l’utilisation du singulier dans les cas où l’entité en question est unique sans aucun doute possible et où le pluriel aurait été du coup confusant.

Par exemple :

  • Pour faire des opérations sur le mot de passe d’un utilisateur : /users/{userId}/password

 

Pour la casse, on privilégie le lowercase ou le snake_case en gardant en tête que la seule véritable règle est d’appliquer la même casse partout.



Un autre casse-tête récurrent est le chaînage de ressources qui sont liées entre elles. Doit-on accéder à une ressource directement avec son identifiant, ou bien passer par sa ressource parente si elle existe ? Pour cela il faudra faire au cas par cas.

En prenant en exemple l’API de Spotify :

  • /users/{user_id}/playlists/{playlist_id} est correct car une playlist est toujours liée à un utilisateur unique,
  • /artists/{artist_id}/albums/{album_id} ne serait pas approprié car un album peut être lié à plusieurs artistes. On privilégie donc /albums/{id}.

 

 

 

En dehors de la logique fonctionnelle du chaînage, il faut également penser aux mécanismes de validation : faut-il valider la cohérence de chaque maillon de la chaîne ou bien se concentrer sur l’objet final de la requête.

Reprenons l’exemple cité ci-dessus qui permet de récupérer une playlist sur Spotify :

  • /users/{user_id}/playlists/{playlist_id}

 

Il est fort probable que l’identifiant de la playlist est unique sur toute la plateforme, ce qui veut dire que l’information de l’utilisateur à qui appartient cette playlist n’est pas indispensable pour la retrouver. Cela ajoute seulement des contraintes de validation supplémentaires pour vérifier que l’utilisateur renseigné existe réellement et qu’il est bien l’auteur de la playlist rattachée.

Sur notre projet, nous avons donc décidé que lorsque l’on se positionne directement sur une entité connue grâce à son identifiant, on peut se passer de l’évocation de son parent.

On aurait donc définit nos routes comme ceci :

  • Pour récupérer les playlists d’un utilisateur : GET /users/{user_id}/playlists
  • Pour créer une nouvelle playlist : POST /users/{user_id}/playlists
  • Pour récupérer une playlist en particulier : GET /playlists/{playlist_id}
  • Pour mettre à jour une playlist : PUT /playlists/{playlist_id}

 

Paramètres

Pour passer des paramètres à une API, différents choix s’offrent à nous. Voici comment il est recommandé de les utiliser :

  • Path : lorsqu’il s’agit d’un paramètre obligatoire, de l’identifiant d’une ressource
  • Query : lorsqu’il s’agit d’un paramètre optionnel
  • Body : pour des données à envoyer
  • Header : pour un paramètre global à toute la plateforme (par exemple le token d’authentification de l’utilisateur sur notre application)

 

Statuts HTTP

Que vous soyez plutôt chien, chat, ou rap US, internet a les aides visuelles pour vous aider à renvoyer le statut correspondant à votre besoin.

Dans sa conférence, Guillaume Laforge propose ce résumé des statuts HTTP :

  • 1XX : Attends…
  • 2XX : Tiens !
  • 3XX : Vas-t-en !
  • 4XX : Tu as fait une erreur
  • 5XX : J’ai fait une erreur

Il est donc conseillé d’utiliser le statut approprié à chaque situation, mais on voit tout de même émerger des anti-patterns parmi les API les plus connues qui renvoient parfois un statut 200 avec un bloc détaillant le cas d’erreur dans la réponse.

 

Erreurs

Les statuts HTTP permettent donc de prévenir les applications consommatrices de l’API en cas d’erreurs mais la granularité n’est généralement pas assez fine pour concevoir une gestion des erreurs uniquement basée dessus.

Il est donc nécessaire de mettre en place un format d’échange qui permettra de gérer les erreurs plus finement, mais il n’existe malheureusement pas à ce jour de réelle standardisation. Certains se sont tout de même penchés sur le sujet et proposent un format à utiliser comme http-problem ou vnd.error.

Sur notre projet, nous nous sommes basés sur le code HTTP 422 (Unprocessable Entity) pour signaler aux consommateurs de l’API qu’il s’agit d’une erreur fonctionnelle. Ils reçoivent dans ce cas une erreur formalisée avec un message sous la forme d’une phrase en anglais détaillant l’erreur rencontrée (pour aider le debug), et un bundle qu’ils pourront utiliser pour proposer un message traduisible à leurs utilisateurs.

 

One size fits all

Une des problématiques des API modernes est qu’elles peuvent être appelées indifféremment depuis un site web ou depuis un mobile.

Ces deux canaux ayant des besoins différents, il faut s’efforcer de répondre à ces besoins sans développer une même fonctionnalité plusieurs fois.

Pour limiter les données envoyées sur mobile tout en gardant un maximum d’informations à envoyer aux sites web, plusieurs possibilités :

  • Filtrer selon les champs qui nous intéressent : concrètement il s’agit de passer à la requête une liste des champs que l’on veut inclure (ou exclure).
  • Spécifier un “style” : plutôt que de lister les champs explicitement, on prédéfinit ces listes dans des “styles” que l’on passe ensuite dans la requête. Exemple : ?style=compact
  • Utiliser le header “Prefer”. Exemple : “Prefer: return=minimal”.

 

Pour notre projet qui comprend un site web frontoffice, un site web backoffice, et une application mobile consommant tous trois la même API, nous avons prévu un point d’entrée différent (une “gateway”) pour chaque plateforme. Cela permet de gérer plus finement les besoins de chacun et d’agréger les données différemment en fonction de ceux-ci :

  • Pour le service gérant l’application mobile, on se soucie de minimiser le nombre d’appels pour contrecarrer les éventuels problèmes de connectivité, et de ne renvoyer que les informations nécessaires,
  • Pour le service gérant le site web, on se soucie un peu moins de la taille de la trame renvoyée et on essaie de donner davantage d’informations à afficher,
  • Pour le service gérant le backoffice, le nombre d’appels n’est pas un souci mais on fera plus attention au volume des données qui peut grossir très rapidement.

 

 

Cette pratique a tout de même quelques inconvénients puisqu’elle impose de dupliquer le code du modèle retourné à plusieurs endroits. Il faut également résister à la tentation d’implémenter des règles métier dans ces gateways : elles ne doivent servir qu’à orchestrer des appels vers les services internes qui portent l’intelligence de l’application, et agréger les données renvoyées par ces derniers.

 

Versioning

Le versioning de l’API se fait très généralement dans l’URL : (par exemple : https://api.spotify.com/v1/recommendations), mais on voit également émerger l’utilisation du header custom X-API-VERSION.

Attention tout de même à l’utilisation d’un header qui peut poser problème lorsqu’on veut utiliser une méthode de l’API en mettant directement son url comme destination d’un lien.

 

Navigation

Afin que les consommateurs de l’API aient envie de l’utiliser et comprennent rapidement comment elle fonctionne, il est important de penser à une solution de navigation qui permette de présenter les différentes méthodes disponibles, et de pouvoir éventuellement les tester directement depuis cette interface.

Guillaume Laforge conseille à cet effet la plateforme DHC by Restlet qui propose, en plus des fonctionnalités de navigation, des outils de tests avancés.

Sur notre projet nous utilisons swagger qui nous permet de documenter nos APIs grâce à un système d’annotations et ensuite de visualiser la documentation générée et utiliser l’API sur l’interface swagger-ui.

 

Conclusion

Cette conférence a donc permis de poser de façon claire et concise un ensemble de bonnes pratiques concernant le design des API REST.

Il ne faut cependant pas oublier que ce sont des bonnes pratiques et non des règles figées, ce sont davantage des pistes de réflexion qui peuvent être adaptées aux besoins de chaque projet. Il est bien sûr possible de traiter ces points au fil de l’eau comme nous l’avons fait sur notre projet, mais il est bien plus simple d’en discuter avec son équipe au préalable afin de garder une certaine cohérence sur l’intégralité de l’API.