Quand une modification d'API casse ce dont vos utilisateurs ignoraient dépendre

Vous déployez une nouvelle version de votre service backend. Le pipeline est vert. Les logs sont propres. Cinq minutes plus tard, votre chat d'équipe s'enflamme : l'application mobile affiche des écrans vides, le frontend web montre des données manquantes, et le service d'une autre équipe renvoie des erreurs 500 à chaque requête.

Que s'est-il passé ? Vous avez changé le nom d'un champ de nama à full_name dans la réponse de l'API. Cela semblait inoffensif. Mais l'application mobile analysait ce champ par son nom, le frontend web l'affichait directement, et le service de l'autre équipe le mappait à une colonne de leur base de données. Aucun d'eux n'a été informé parce que vous ignoriez leur existence.

C'est la réalité de la maintenance des API. Contrairement à un worker en arrière-plan ou à une tâche planifiée que seule votre équipe touche, une API a des consommateurs dont vous ignorez peut-être l'existence. Une application mobile sortie l'année dernière. Une intégration partenaire mise en place par un autre service. Un frontend qui ne peut pas être mis à jour avant le prochain passage en revue de l'App Store. Quand vous modifiez l'API, vous ne modifiez pas seulement votre code. Vous modifiez le contrat sur lequel d'autres systèmes reposent.

Le problème que vous ne pouvez pas voir

Le problème central est la visibilité. Dans une grande organisation, un seul point d'accès API peut être consommé par des dizaines d'équipes. Même si votre API ne sert qu'une seule application, cette application est peut-être déjà entre les mains d'utilisateurs qui ne peuvent pas se mettre à jour immédiatement. Vous ne pouvez pas coordonner un déploiement simultané sur tous les consommateurs. Certains, vous ignorez même leur existence.

C'est là que la rétrocompatibilité devient une nécessité pratique, pas un idéal théorique. La rétrocompatibilité signifie qu'une nouvelle version de votre API peut toujours répondre aux requêtes en utilisant le même format que l'ancienne version. Si votre point d'accès /users renvoyait auparavant un champ appelé nama, la nouvelle version ne peut pas le renommer soudainement en full_name sans période de transition. Si le paramètre page acceptait un entier, la nouvelle version ne peut pas soudainement exiger une chaîne de caractères. Ces changements brisent le contrat, et des contrats brisés signifient des consommateurs brisés.

Ce qui constitue un changement cassant

Les changements cassants prennent de nombreuses formes, et certains sont plus évidents que d'autres. Supprimer un point d'accès est clairement cassant. Renommer un champ est cassant. Changer un type de données est cassant. Ajouter un champ obligatoire à un corps de requête est cassant. Changer le format des réponses d'erreur est cassant.

Mais il y en a de plus subtils. Changer l'ordre des champs dans une réponse JSON peut casser du code qui repose sur l'indexation de tableau. Ajouter un nouvel en-tête obligatoire peut casser les clients qui ne l'envoient pas. Même changer le code de statut HTTP pour une condition d'erreur spécifique peut casser une logique qui vérifie des valeurs de statut exactes.

Considérez cet exemple simple. Votre API renvoyait auparavant un objet utilisateur comme ceci :

{
  "id": 42,
  "nama": "Ani Wijaya",
  "email": "ani@example.com"
}

Après votre renommage de "nettoyage", elle renvoie :

{
  "id": 42,
  "full_name": "Ani Wijaya",
  "email": "ani@example.com"
}

Pour vous, c'est un meilleur nom. Pour l'application mobile qui analyse response["nama"], c'est undefined. Pour le frontend qui affiche user.nama, c'est vide. Pour le service partenaire qui mappe nama à sa colonne de base de données, c'est un null silencieux. Le champ existe toujours en esprit, mais le contrat est brisé.

La partie délicate est que ces changements semblent souvent inoffensifs du côté serveur. Vous nettoyez, ajoutez une fonctionnalité ou corrigez une incohérence de nommage. Mais du point de vue du consommateur, le comportement a changé d'une manière qu'il n'a pas demandée et à laquelle il ne peut pas s'adapter sans modification de code de son côté.

Attrapez les changements cassants avant qu'ils n'atteignent la production

L'approche la plus sûre est de détecter automatiquement les changements cassants dans votre pipeline CI. Vous ne voulez pas compter sur une relecture manuelle ou espérer que quelqu'un se souvienne de vérifier. Il existe des outils conçus spécifiquement pour cela.

Si vous utilisez un format de spécification d'API comme OpenAPI, des outils comme OpenAPI Diff ou Spectral peuvent comparer les versions ancienne et nouvelle de votre spécification. Ils signalent exactement ce qui a changé et si c'est un changement cassant. Si votre framework génère automatiquement la spécification, comme FastAPI, SpringDoc ou ASP.NET Core avec Swashbuckle, vous pouvez exécuter cette comparaison sur chaque pull request.

Le flux de travail ressemble à ceci : lorsqu'un développeur ouvre une pull request, le pipeline CI construit la nouvelle spécification d'API, la compare à la spécification de la branche principale, et rapporte tout changement cassant. S'il n'y en a pas, la PR peut continuer normalement. S'il y a des changements cassants, l'équipe peut discuter si le changement est vraiment nécessaire ou s'il peut être fait de manière incrémentale.

Cela déplace la conversation de "j'espère que ça ne casse rien" à "voici exactement ce qui a changé et si cela brise le contrat." Cela transforme un risque invisible en un point de décision visible.

Quand vous ne pouvez pas éviter les changements cassants

Tous les changements cassants ne peuvent pas être évités. Parfois, les exigences métier imposent une refonte. Parfois, l'architecture doit évoluer. Parfois, l'ancienne conception était tout simplement erronée et doit être remplacée.

Lorsque vous devez effectuer un changement cassant, le versionnage d'API est l'approche standard. L'idée est simple : vous maintenez plusieurs versions de votre API simultanément. Les anciens consommateurs continuent d'utiliser l'ancienne version tandis que les nouveaux consommateurs adoptent la nouvelle version. Vous donnez à tout le monde le temps de migrer.

Il existe plusieurs façons d'implémenter le versionnage, chacune avec des compromis.

Le versionnage par URL est l'approche la plus courante. Vous placez la version dans le chemin : /v1/utilisateurs et /v2/utilisateurs. C'est facile à comprendre, facile à router et facile à tester. L'inconvénient est que les URL deviennent plus longues et que vous maintenez des chemins de code séparés pour chaque version.

Le versionnage par en-tête garde l'URL propre mais oblige les consommateurs à définir un en-tête personnalisé, comme Accept: application/vnd.maapi.v1+json. C'est plus RESTful en théorie, mais cela ajoute de la complexité pour les consommateurs qui doivent configurer correctement les en-têtes.

Le versionnage par paramètre de requête utilise quelque chose comme ?version=1. C'est simple mais moins courant car cela peut encombrer les chaînes de requête et est plus difficile à mettre en cache correctement.

Quelle que soit la méthode choisie, le versionnage n'est pas gratuit. Chaque version que vous maintenez est du code que vous devez exécuter, tester, déboguer et éventuellement déprécier. Vous avez besoin d'une politique pour la durée de prise en charge des anciennes versions. Six mois est courant. Un an est généreux. Quoi que vous choisissiez, communiquez-le clairement et bien à l'avance. Donnez aux consommateurs une fenêtre de migration, pas un arrêt surprise.

La meilleure voie : concevoir pour l'évolution

Le versionnage est un filet de sécurité, pas une stratégie. La meilleure approche est de concevoir votre API pour qu'elle puisse évoluer sans avoir besoin de nouvelles versions. Cela signifie être délibéré sur ce que vous exposez et comment vous l'exposez.

Ne renvoyez pas de champs dont les consommateurs n'ont pas besoin. Chaque champ que vous ajoutez à une réponse est un champ dont quelqu'un pourrait dépendre. Si vous n'êtes pas sûr qu'un champ soit utile, laissez-le de côté. Vous pouvez toujours l'ajouter plus tard, mais le supprimer est un changement cassant.

Utilisez des types de données flexibles lorsque c'est possible. Acceptez à la fois les chaînes et les nombres pour les paramètres qui pourraient raisonnablement être l'un ou l'autre. Renvoyez des champs supplémentaires dans les réponses sans obliger les consommateurs à les utiliser. Le principe est simple : soyez libéral dans ce que vous acceptez et conservateur dans ce que vous promettez.

Lorsque vous devez ajouter de nouvelles fonctionnalités, ajoutez de nouveaux champs ou de nouveaux points d'accès. Ne modifiez pas ceux qui existent. Un nouveau champ dans une réponse ne casse pas les anciens consommateurs car ils l'ignorent simplement. Un nouveau point d'accès ne casse pas les anciens consommateurs car ils ne l'appellent jamais. Tant que vous ne supprimez ou ne renommez pas les éléments existants, vous pouvez continuer à évoluer sans incrémenter les versions.

Liste de contrôle pratique pour les modifications d'API

Avant de fusionner cette pull request, passez en revue ces vérifications :

  • Avez-vous supprimé ou renommé un champ, un paramètre ou un point d'accès existant ?
  • Avez-vous changé le type de données d'un champ ou d'un paramètre existant ?
  • Avez-vous ajouté un champ obligatoire au corps de la requête ?
  • Avez-vous changé le format des réponses d'erreur ?
  • Avez-vous changé les codes de statut HTTP pour des scénarios existants ?
  • Avez-vous exécuté une comparaison automatique avec la spécification d'API précédente ?

Si vous avez répondu oui à l'une des cinq premières questions, vous avez un changement cassant. Décidez si vous devez le reporter, le versionner ou le communiquer clairement à tous les consommateurs connus.

L'essentiel à retenir

Votre API est un contrat. Chaque consommateur qui en dépend a passé un accord implicite basé sur son comportement actuel. Changer ce contrat sans prévenir n'est pas une erreur technique. C'est un échec de coordination qui érode la confiance entre les équipes.

L'objectif n'est pas de ne jamais faire de changements cassants. L'objectif est de savoir quand vous en faites un, de décider consciemment s'il en vaut la peine, et de donner à vos consommateurs une voie de progression. Automatisez la détection, communiquez le changement et concevez pour l'évolution dès le départ. Vos consommateurs ne vous remercieront jamais de maintenir la rétrocompatibilité, mais ils remarqueront certainement quand vous ne le faites pas.