Concevoir une API dont le contrat tient dans le temps
Vincent
Co-fondateur, direction technique

En bref
Comment concevoir une API qui pourra évoluer sans casser ses clients ?
En traitant le contrat comme une promesse : on n'enlève jamais un champ, on ne change jamais le sens d'un champ existant, et on n'ajoute jamais de champ obligatoire. Toute évolution se fait par ajout de champs optionnels. Le versionnage n'est nécessaire que pour les ruptures qu'on n'a pas su éviter — et chaque version supplémentaire est une dette permanente, puisqu'il faut la maintenir aussi longtemps que quelqu'un l'utilise.
Une API interne se corrige : on prévient les trois équipes concernées et on livre ensemble. Une API consommée par des tiers ne se corrige pas — chaque changement doit être compatible, ou il casse quelqu’un dont on ne connaît pas le calendrier.
Cette contrainte change la conception, et elle est plus stricte qu’on ne l’imagine au départ.
Les trois règles de compatibilité
Elles tiennent en trois interdits, et elles couvrent l’essentiel des ruptures accidentelles.
Ne jamais retirer un champ. Même s’il paraît inutilisé. Vous ne savez pas qui le lit, ni ce qu’il en fait. Un champ devenu obsolète se documente comme tel et se laisse en place ; il coûte quelques octets, et son retrait coûte un incident chez un client.
Ne jamais changer le sens d’un champ existant. Un champ statut qui passait de trois valeurs
possibles à cinq casse tous les clients qui traitaient les trois cas de façon exhaustive. Idem
pour une unité qui passe des euros aux centimes, ou une date qui change de format. Le nom reste,
le sens change, et personne ne le voit venir — c’est la rupture la plus vicieuse.
Ne jamais ajouter un champ obligatoire en entrée. Un client existant, qui ne l’envoie pas, se met à recevoir une erreur. Tout nouveau champ doit être optionnel, avec un comportement par défaut identique à l’ancien.
Ces trois règles suffisent à faire évoluer une API pendant des années sans jamais casser personne.
Ce qui oblige malgré tout à rompre
Il reste des cas, et ils viennent presque toujours d’une erreur de conception initiale.
Un modèle de données trop plat. Une API qui expose un client avec une seule adresse, et le
métier en veut désormais plusieurs. Le champ adresse ne peut pas devenir une liste sans
rupture. La parade : anticiper la cardinalité — quand un objet peut raisonnablement en avoir
plusieurs un jour, l’exposer comme une liste dès le départ, même à un seul élément.
Des valeurs énumérées non extensibles. Si le contrat dit que statut vaut exactement A, B
ou C, ajouter D est une rupture. La parade : documenter dès le départ que la liste peut
s’enrichir, et demander aux clients de traiter les valeurs inconnues sans échouer.
Des identifiants porteurs de sens. Un identifiant qui encode l’année ou le type dans sa forme finit toujours par devoir changer de format. Un identifiant opaque n’a pas ce problème.
Le versionnage, en dernier recours
Quand la rupture est inévitable, on version. Mais chaque version est une dette permanente : il faut la maintenir, la corriger, la documenter, tant que quelqu’un l’utilise — c’est-à-dire longtemps.
Ce que nous appliquons : une version majeure dans le chemin d’accès, deux versions vivantes au maximum, une date de fin annoncée dès la sortie de la nouvelle, et une mesure de l’usage réel de l’ancienne pour savoir qui prévenir.
Ce dernier point est indispensable. Retirer une version sans savoir qui l’utilise revient à couper une ligne au hasard.
Photo : Evan Phillip — licence CC0.
Les erreurs, qui font partie du contrat
Un point systématiquement négligé : la façon dont une API échoue est aussi contractuelle que la façon dont elle réussit.
Un client construit sa logique de reprise sur les codes d’erreur. Changer un code, ou renvoyer un format différent selon le cas, casse cette logique aussi sûrement qu’un champ retiré.
Nous fixons donc trois choses dès le départ : un format d’erreur unique pour toute l’API, un code stable et documenté par type d’erreur, et une distinction nette entre ce qui est réessayable et ce qui ne l’est pas. Sans cette distinction, un client prudent réessaie tout — y compris une opération déjà passée.
Ce qui rend une API agréable à consommer
Au-delà de la compatibilité, quelques choix font une grande différence pour ceux qui l’utilisent.
La cohérence avant l’élégance. Mieux vaut une convention moyenne appliquée partout qu’une convention parfaite appliquée à moitié. Un consommateur apprend vos règles une fois et les applique ensuite sans réfléchir.
Une pagination dès le premier jour. Une liste qui renvoie tout fonctionne avec cinquante éléments et s’écroule avec cinquante mille. Ajouter la pagination après coup est une rupture.
Des exemples réels dans la documentation. Une requête complète et sa réponse complète valent mieux qu’une description exhaustive de chaque champ. C’est ce que les gens copient.
L’idempotence sur les écritures. Permettre au client de rejouer une création sans risquer un doublon simplifie énormément sa gestion d’erreur — et évite les doubles commandes, qui sont un problème métier, pas technique.
Photo : Lance Asper — licence CC0.
La pagination et les grands volumes
C’est le sujet qui rattrape le plus souvent les API conçues avec de petits jeux de données.
Une liste qui renvoie tout fonctionne parfaitement avec deux cents enregistrements et devient inutilisable à cinquante mille. Le problème n’est pas seulement le temps de réponse : c’est la mémoire consommée côté serveur, et le fait qu’un seul appel puisse dégrader le service pour tout le monde.
Deux points que je considère comme non négociables dès la première version.
Une pagination obligatoire, avec une limite par défaut. Si le client ne précise rien, il reçoit une page. Rendre la pagination facultative revient à ne pas en avoir, parce que personne ne l’utilisera tant que les volumes seront petits.
Une pagination par curseur plutôt que par numéro de page dès que les données changent pendant la lecture. Avec des numéros de page, un enregistrement inséré pendant le parcours décale tout et fait apparaître des doublons ou des oublis — un défaut particulièrement pénible parce qu’il est intermittent.
La documentation fait partie de l’interface
Une API dont la documentation est fausse est plus coûteuse qu’une API sans documentation : elle inspire une confiance qu’elle ne mérite pas.
La seule approche qui tienne dans la durée est de générer la documentation depuis le code, à partir de la description formelle des routes et des types. Une documentation écrite à la main diverge en quelques semaines, sans que personne ne s’en aperçoive.
Trois éléments manquent presque toujours, et ce sont ceux qu’on cherche en premier quand on consomme une API : un exemple de requête complet, y compris l’authentification ; la liste des codes d’erreur possibles pour chaque route ; et les limites de débit appliquées, avec ce qui se passe quand on les dépasse.
Ce dernier point mérite d’être explicite. Une API qui limite sans le documenter transforme une contrainte normale en incident inexplicable pour celui qui la consomme.
La question à se poser avant de publier
Une seule : suis-je prêt à maintenir ce contrat pendant cinq ans ?
Si la réponse est non, il vaut mieux restreindre ce qu’on expose. Une API réduite qu’on peut tenir vaut infiniment mieux qu’une API généreuse qu’on devra casser dans dix-huit mois.