Si vous êtes habitué aux pratiques crafts, que vous avez l'habitude de gérer des APIs REST en prod depuis plusieurs années, en particulier avec des obligations de zero-downtime, je pense que je vais ici enfoncer des portes ouvertes. Mais je vois encore et toujours les mêmes erreurs, donc je me dis qu'il est temps que je pose tout ça par écrit pour le partager !
Note 1 : écrire une excellente API REST c'est un art compliqué qui demande du travail, de l'expérience et de lire beaucoup ! Je ne vais pas rentrer dans tous les éléments "avancés" comme le HATEOAS, mon but ici c'est de pousser vers des pratiques simples qui sont pour moi le minimum pour une API REST qui tient la route. Il existe pas mal de gens qui font du contenu bien plus avancé, je vous mettrais quelques liens dans les sources.
Note 2 : ici je vais parler d'API REST, si vous construisez des API GraphQL ou gRPC, ça ne s'appliquera pas, c'est une autre logique.
Pourquoi ?
Dans cet article je vais vous proposer des points à améliorer pour vos APIs, mais comme ça implique de changer un peu vos habitudes, je trouve ça important de vous dire pourquoi. Sur chaque point j'indiquerais ensuite quels éléments ça impact.
Le premier élément pour moi c'est l'évolutivité ou la capacité à facilement faire évoluer l'API sans trop se casser la tête, en minimisant les refactoring, à être compris et pris en main par un nouveau développeur.
Le second élément c'est la facilité à consommer l'API. J'ai malheureusement l'impression qu'on oublie souvent qu'on fait une API pour des consommateurs et qu'en tant que consommateur d'une API, on a pas envie de comprendre comment elle fonctionne sous le capot mais seulement de consommer l'API.
Ensuite vient le zéro downtime. L'idée de s'assurer que pendant qu'on met à jour l'API en production (pensez par exemple que vous avez 4 instances de l'API, vous les remplacez une par une par la nouvelle version) vous ne voulez pas qu'un consommateur puisse voir à cause d'erreur (typiquement parce que je change d'API) que l'API est mise à jour. Ça c'est important ou pas selon votre cas, mais je pense que viser la capacité à faire du zéro downtime sans en avoir besoin c'est plus simple que ne pas le viser de base que le besoin arrive ensuite…
Le dernier élément c'est la scalabilité. Si vous structurer correctement votre API, c'est facile ou non d'ajouter des nouveaux endpoints, découper l'API en plusieurs services, agréger des APIs, mettre une façade (type Spring Gateway avec des règles), etc.
Pensez endpoints
#facilité-à-consommer #évolutivité #scalabilité
Quand on parle d'API REST, il faut absolument prendre un mindset "resources". /getUser
c'est une URL qui peut être parfaitement fonctionnelle, ça ne sera jamais REST.
L'idée derrière REST (REpresentational State Transfer) c'est que chaque resource va être représenté par une URL unique sur laquelle on va venir appeler des verbes pour effectuer des actions.
Par exemple, si vous voulez créer une API pour un CMS de blog, vous pourriez avoir une URL /api/v1/posts
, sur lequel on peut appliquer des verbes :
GET /api/v1/posts => va donner la liste de tous les posts
POST /api/v1/posts => créer un post de blog
PUT /api/v1/posts => met à jour le post de blog ou le créer s'il n'existe pas
GET /api/v1/posts/{postId} => va donner le post qui a pour id postId
PUT /api/v1/posts/{postId} => modifier le post qui a pour id postId en passant l'objet complet
PATCH /api/v1/posts/{postId} => modifier le post qui a pour id postId en passant uniquement les parties de l'objet qui doivent changer
DELETE /api/v1/posts/{postId} => supprimer le post qui a pour id postId
Vous n'aurez pas ça pour chaque API, mais c'est pourtant ça qu'on attend d'une API, c'est pourtant ça qui est indiqué dans les spécifications, c'est ça qu'on est en droit d'attendre quand on consomme une API REST. Il n'y a pas d'obligation de supporter tous les verbes pour chaque endpoint, par contre respecter la sémantique des verbes c'est important pour le consommateur mais aussi pour faire évoluer l'API, particulièrement pour des nouveaux développeurs qui sauront directement s'orienter dans l'API.
Pour moi ça participe aussi à la scalabilité au sens où c'est facile de répliquer ce modèle pour toutes les ressources qu'on va exposer et c'est aussi facile d'ajouter des outils qui aident à la scalabilité (par exemple, une API Gateway) pour gérer la sécurité en amont de vos API. Ces outils sont souvent plus simples et rapides à configurer si vous suivez les bonnes pratiques REST sur la construction des endpoints.
Par contre il y a des exceptions, des cas où on peut difficilement penser "ressource", des cas où on veut effectuer une action côté serveur qui n'est pas vraiment une modification d'une ressource ou des cas où on veut que le serveur valide des données sans rien faire de plus. Dans ces cas-là je vous conseille d'utiliser au choix GET (si pas besoin du body dans la requête) ou PUT (si l'action est idempotente (= peut être reproduit sans changer la réponse)), d'utiliser un verbe dans l'URL, et d'associer l'action à la ressource au mieux.
Deux exemples :
PUT /api/v1/posts/prevalidate => on passe le même objet que pour la création mais aucune donnée n'est sauvegardé, on sait juste si ce qui est déjà rempli pourrait être accepté pour une création
GET /api/v1/links/grab-metadata/{url} => on passe une URL, le serveur va récupérer des infos sur la page
Une nomenclature uniforme
#facilité-à-consommer #évolutivité #scalabilité
Quand bien même on pense ressource et endpoint, c'est important de penser aussi à l'uniformisation.
Avoir une nomenclature uniforme sur l'ensemble de votre API (voir de toutes les APIs, mais il n'existe pas toujours de règles claires au niveau de l'entreprise), de préférence en suivant les conventions générales, ça permet de plus facilement consommer une API en se posant moins de question.
Quelques conventions qu'on retrouve un peu partout et qui sont faciles à suivre :
- commencer le chemin par
/api
: ça permet de distinguer les endpoints métiers des endpoints techniques (par exemple avec Spring vous aurez/actuator
qui regroupera pas mal de endpoints techniques) ; - avoir la version du endpoint dans l'URL au plus tôt
/api/v1
; - tous les noms de ressources au pluriel : par défaut on est censé renvoyer une collection, donc le pluriel est logique ;
- éviter au maximum les verbes dans URL (sauf pour les actions qui ne sont pas REST) ;
- utiliser une seule case partout (de préférence kebab case) ;
Exemples de bonne URL :
/api/v1/posts
/api/v1/posts/{postId}
/api/v1/posts/{postId}/tags
/api/v1/posts/{postId}/comments
/api/v1/posts/{postId}/comments/{commentId}
Ici quand vous parcourez les URLs, vous voyez une certaine logique, le formatage est prévisible, on ne se pose pas de question à l'usage.
Exemples de mauvaises URL :
/posts/ => pas versionné
/api/v1/posts/tags/{postId} => l'id de post n'est pas au bon endroit ça n'est pas logique
/api/v1/blogPost/{postId}/tags => le casing n'est pas constant, alternance singulier/pluriel
Là il faut être attentif à chaque caractère, aucune règle ou logique.
Les codes HTTP sont vos amis !
#facilité-à-consommer #évolutivité
Quand je construis une API REST, je fais très attention aux codes d'erreurs. Il y a une spécification qui défini la signification de chaque code d'erreur. Si vous respecter les codes d'erreurs, ce sera plus simple pour les consommateurs de savoir ce qui se passe et de traiter l'erreur. Ce sera aussi beaucoup plus facile de gérer des tout l'outillage autour de votre API, la plupart des systèmes étant de base fait pour gérer des codes de retours standards. Il est aussi à noter que certains outils vous demanderont un effort supplémentaire si vous ne respecter par ces codes au moment de consommer une API.
Globalement, on a 5 catégories de code :
- 1xx : information en attendant de pouvoir donner une vraie réponse (très très peu utilisé) ;
- 2xx : quand le service a été rendu ;
- 3xx : pour les redirections vers ailleurs ;
- 4xx : pour indiquer à l'utilisateur que sa requête a un problème (ressource qui n'existe pas, donnée invalide, etc.), souvent résumé en "You fucked up!";
- 5xx : pour indiquer une erreur côté serveur, souvent résumé en "I fucked up!" ;
Sans être exhaustif, je peux vous donner une idée des codes de retour qu'on attend généralement en fonction du verbe utilisé.
Déjà sur un peu tous les verbes, on s'attend globalement à des 401, 403 et 500. Une erreur 401 signifie qu'on a pas fourni une identification valide, c'est très variable en fonction de votre fonctionnement mais généralement il faut regarder du côté du header Authorization qui devrait contenir un token / un basic auth valide. L'erreur 403 indique que l'API a bien reconnu l'utilisateur, mais que celui-ci n'a le droit d'accéder à la ressource. Une erreur 500 pour indiquer qu'il y a eu une erreur côté serveur et qu'elle n'a pas été traitée.
Côté GET, les codes qu'on verra la plus sont 200 et 404. Le côté 200 indique que tout va bien, pour un GET ça veut globalement dire "tiens voilà ta donnée". L'erreur 404 indique que la ressource demandée n'existe pas.
Pour le POST, on va s'attendre généralement à 201 et 400. Pour la création de ressource on préférera un 201 qui indique que la ressource a été créée, à un 200 indiquant juste un "OK" générique. Pour toute erreur dans les informations fournis, on renverra un 400 pour indiquer à l'utilisateur que c'est lui qui s'est trompé.
Côté PATCH et PUT on s'attend à un 200, 204, 400 ou 404. Les deux premiers pour indiquer qu'on a bien mis à jour en fonction de si on a un contenu à renvoyer ou non. Le 404 s'ajoute car on vient modifier une ressource qui pourrait être inexistante. Pour le PUT on pourra aussi retrouver couramment 201 dans le cas où on donne dans le body un objet complet qui n'existe pas.
Pour le DELETE, on verra souvent 200, 204 ou 404. L'erreur 404 si on ne trouve pas la ressource qu'on veut supprimer. 200 ou 204 en fonction de si on a un contenu à renvoyer ou non : 204 si on ne renvoie rien dans le body, 200 si par exemple on renvoie l'objet qui a été supprimé.
J'ai volontairement passé tous les codes 3xx (301, 302, etc.), la plupart des erreurs 4xx (402, 405, 418, 431, etc.) et les codes 5xx (502, 504, etc.), mon but ici n'est pas de faire une documentation des codes HTTP, mais juste reposer les bases, n'hésitez pas à aller voir une liste plus complète comme basiquement celle présente sur Wikipédia.
Versionner son API
#facilité-à-consommer #évolutivité #zero-downtime
Quand on commence une API on ne sait jamais si on aura besoin de versioning. On espère même ne jamais en avoir besoin en fait, car maintenir plusieurs versions est toujours un calvaire… Mais au cas où c'est beaucoup mieux de commencer en versionnant, quitte à rester en v1 ad vitam æternam !
Donner une version à son API ça permet de faire évoluer l'API de manière claire, indiquer explicitement qu'on change de manière cassante le comportement, et donc pour passer à la version suivante il faut que les utilisateurs s'adaptent.
C'est toujours mieux de faire en sorte de casser le moins possible, mais parfois c'est sain de faire un changement cassant et pousser les consommateurs à migrer dans un délai raisonnable pour garder une API (et la base de code qui est derrière) saine.
Ça aide aussi à garantir du zero-downtime de versionner correctement son API : imaginez que vous deviez déployer une API qui introduit un changement cassant sans versionnement, il faut synchroniser la mise en production de l'API avec la mise en production de tous les consommateurs… Si on a un versionnement, on va pouvoir pousser la nouvelle version de l'API, comme l'ancienne est toujours active sur l'API, les consommateurs ne voient aucune différence, là on peut pousser les consommateurs à migrer vers la nouvelle version, quand tous les consommateurs ont migré on peut supprimer l'ancienne.
Toujours renvoyer un objet
#évolutivité #zero-downtime
Règle de base : le besoin va toujours changer. Donc il faut prévoir de pouvoir s'adapter un minimum le plus facilement possible.
Imaginons que vous ayez un endpoint GET /api/v1/posts/
et que vous renvoyez directement une liste de post (comprendre : le body contient [{ "id": "...", ... }]
). Demain vous avez besoin d'ajouter des métadonnées comme le nombre de posts, des informations de pagination, la liste des tags des articles dans la liste, etc. Comment vous allez faire ?
Le cas classique ce serait de changer le type de retour, passer un objet qui aurait une clé "posts"
avec la liste des posts qu'on renvoyait avant, et d'autres clés avec les métadonnées. Mais du coup là vous avez fait un changement cassant, donc vous devez incrémenter la version de l'API, sinon tous les consommateurs vont être en erreur au moment de la mise en production (ou vous allez devoir faire un jeu de synchronisation entre vous et les consommateurs pour que la mise en production soit coordonnée…), et donc vous allez devoir faire vivre les deux versions de l'API pendant un certain temps le temps que les consommateurs s'adaptent. En fonction des cas vous en avez pour quelques semaines à quelques mois pour qu'il n'y ait plus de consommateur de l'ancienne version pour la supprimer.
Et ce sera comme ça pour tous les endpoints où vous renvoyez autre chose qu'un objet JSON. Partout où vous allez renvoyer un booléen, une chaîne de caractère, une liste, un nombre, vous ne pouvez pas le faire évoluer librement au besoin.
La solution est pourtant très simple : chaque endpoint ne doit retourner que des objets manipulables. Peut-être que pour toujours le retour de GET /api/v1/posts/
sera { "posts" [{"id":"...", ... }] }
, peut-être pas. Le coût de faire ça est négligeable, aussi bien en termes de code que de performance. Le coût de modification si vous ne le faites pas peut se compter en mois.
Conclusion
Si vous appliquez ces conseils plutôt simples à mettre en place (mais qui peuvent un peu changer vos habitudes), vous allez clairement avoir une API de meilleure qualité. Elles ne seront pas parfaites, elles ne seront au plus niveau d'exigence qu'on peut attendre d'une API, mais ce sera déjà beaucoup mieux que beaucoup d'API que j'ai déjà vu...
Source :
- REST
- HATEOAS
- Liste des codes HTTP
- PUT request method (mdn)
- 201 Created (mdn)
- REST next level : Création d'API web orientées métier par Julien Topçu
Crédit photo : Générée via Mistral AI avec le prompt suivant :
Create a split-image illustration showing a before-and-after scenario of a developer at work. On the left side, depict a developer struggling with their work: the desk is cluttered with papers, multiple screens show error messages, and the developer looks stressed with a tired expression, surrounded by empty coffee cups. On the right side, show the same developer in a much better situation: the desk is organized, the screens display successful code execution, and the developer looks confident and relaxed with a smile, working efficiently with fewer distractions. Use a bright and cheerful color palette to emphasize the positive transformation. Ensure the image conveys a sense of improvement and productivity.