Stratégie de versioning d'API
TL;DR : Les versions basées sur une date, épinglées par clé API (modèle de Stripe) battent
/v1/,/v2/pour les API publiques à longue durée de vie, car elles permettent de faire de petites modifications cassantes sans forcer les clients à migrer vers une nouvelle arborescence. Pour les API internes, l'évolution par ajout associée aux en-têtesDeprecationetSunset(RFC 9745, RFC 8594) suffit généralement. Annoncez toujours, dépréciez toujours avec une date, ne retirez jamais sans préavis.
Accès rapide par problème
| Symptôme | Section |
|---|---|
| « Besoin de modifier un champ mais j'ai 100 000 clés API » | Versions épinglées à une date |
« Dois-je utiliser /v1/ ou Accept: application/vnd.foo.v2+json ? » | Comparaison des stratégies |
| « Comment indiquer aux clients qu'un point d'accès va disparaître ? » | Deprecation + Sunset |
| « Maintenir 6 versions dans le code nous tue » | Transformateurs de version |
| « API interne — avons-nous vraiment besoin de versioning ? » | Interne vs Public |
Diagramme de décision
flowchart TD
A[Besoins de modifier le comportement de l'API] --> B{Uniquement par ajout ?<br/>nouveau champ, nouveau point d'accès, nouveau paramètre optionnel}
B -->|Oui| C[Publier simplement<br/>pas de versioning nécessaire]
B -->|Non, cassant| D{API publique<br/>+ nombreux clients opaques ?}
D -->|Non, interne/peu de clients| E[Coordonner la migration<br/>+ en-têtes Deprecation/Sunset]
D -->|Oui, publique| F{Pouvez-vous fournir une couche<br/>compatible par requête ?}
F -->|Oui, vous avez un transformateur| G[Version épinglée à une date<br/>Modèle Stripe]
F -->|Non, profonde réécriture| H[Incrémentation du numéro de version majeure dans l'URI<br/>/v1 → /v2]
E --> I[Définir Deprecation: @timestamp<br/>+ Sunset: HTTP-date<br/>+ Link: rel=deprecation]
G --> I
H --> I
1. Versions épinglées à une date (Modèle de Stripe)
Stripe a publié son architecture dans API versioning at Stripe :
La première fois qu'un utilisateur effectue une requête API, son compte est automatiquement épinglé à la version la plus récente disponible, et à partir de là, chaque appel API qu'il effectue se voit attribuer cette version de manière implicite.
Les versions sont des versions glissantes nommées avec la date de leur publication (par exemple,
2017-05-24).
Les propriétés clés :
| Propriété | Pourquoi ça marche |
|---|---|
| Version par défaut par compte (définie au premier appel) | Les nouveaux clients sont automatiquement épinglés à la dernière version ; les clients existants ne sont pas cassés lors de la publication d'une modification |
L'en-tête Stripe-Version écrase l'épingle par requête | Permet une migration progressive du client : le SDK peut adhérer à une nouvelle version avant que le compte ne le fasse |
Noms basés sur une date (2024-04-10) | Indique la récence ; pas de débat sur ce que signifie « v3 » ; permet un nombre arbitraire de petites modifications cassantes au lieu de les accumuler pour une grosse version « v3 » |
| Chemin de mise à niveau via le tableau de bord | Le client peut prévisualiser la différence, puis mettre à niveau la version par défaut de son compte |
C'est la seule approche qui passe à l'échelle pour les API publiques à très longue durée de vie (Stripe a des versions vieilles d'une décennie). Cela vous coûte en complexité interne (le motif transformateur de la §4) mais épargne à vos clients le cycle perpétuel de migration /v1/ → /v2/.
2. Comparaison des stratégies de versioning
| Stratégie | Où se trouve la version | Avantage | Inconvénient | Idéal pour |
|---|---|---|---|---|
Segment d'URI (/v1/, /v2/) | Chemin | Compatible cache (URL différente = entrée de cache différente) ; aucune configuration client | Impossible de faire de petites modifications ; force un fork de l'arborescence ; les URL ne sont plus des « identifiants de ressource stables » (selon Fielding) | API internes, API publiques rarement modifiées |
En-tête personnalisé (Stripe-Version: 2024-04-10) | En-tête de requête | Beaucoup de versions peu coûteuses ; granularité par requête | Impossible de partager des URLs avec la version intégrée ; plus difficile à tester dans la barre d'adresse du navigateur | API publiques à grande échelle |
Type de média Accept (Accept: application/vnd.foo.v2+json) | En-tête Accept standard | « Conforme à la spécification » selon HTTP ; réutilise le mécanisme de négociation de contenu | Difficile à configurer ; prise en charge variable par les outils ; débogage via curl verbeux | API hypermédia, conceptions véritablement REST |
Paramètre de requête (?version=2) | URL | Facile à tester ; visible dans les journaux | Pollue l'URL ; sémantiquement incorrect (la version n'est pas une propriété de la ressource) | Prototypage rapide uniquement |
| Pas de versioning, uniquement par ajout | n/a | Aucun surcoût ; aucune coordination client | Impossible de faire une modification cassante sans un nouveau point d'accès | Microservices internes avec déploiement partagé |
Choisissez en fonction de votre contrainte :
- « Nous faisons de petites modifications cassantes fréquemment » → version basée sur une date via en-tête (Stripe)
- « Nous faisons une grosse réécriture tous les 5 ans » → segment d'URI (
/v1/,/v2/) - « Nous ne cassons jamais, nous ajoutons seulement » → pas de versioning + évolution par ajout
3. La paire d'en-têtes Deprecation/Sunset
Deux RFC de l'IETF couvrent le signal de cycle de vie :
En-tête Sunset — RFC 8594
La valeur Sunset est un horodatage au format HTTP-date, tel que défini dans la Section 7.1.1.1 de [RFC7231], et DEVRAIT être un horodatage dans le futur.
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Indique que « la ressource devrait devenir indisponible à un moment donné dans le futur. » Les clients DEVRAIENT traiter l'horodatage comme une indication, non comme un contrat ferme.
En-tête Deprecation — RFC 9745
Deprecation: @1735689599
Sunset: Sun, 31 Dec 2026 23:59:59 GMT
Link: <https://api.example.com/docs/migrate-v1-to-v2>; rel="deprecation"
La valeur Deprecation est un horodatage Unix (secondes, préfixé par @ conformément aux champs structurés). Il peut être dans le passé (« déjà déprécié ») ou dans le futur (« sera déprécié »). Les DOIT :
- DOIT utiliser le format de date à champ structuré selon RFC 9651
SunsetNE DOIT PAS être antérieur àDeprecation— la spécification est explicite ; c'est une contrainte d'ordre temporel- DEVRAIT inclure un
Linkavecrel="deprecation"pointant vers la documentation de migration
Le fait d'envoyer Deprecation ne change pas le comportement de la ressource — c'est un signal, pas une dégradation. Les serveurs DEVRAIENT maintenir la ressource fonctionnelle jusqu'à la date Sunset (sauf urgence).
Calendrier recommandé
T+0 Première Deprecation : en-tête envoyé en production
T+30j Annonce publique (changelog, email, documentation)
T+90j Ajout de journaux de réponse / métriques sur l'utilisation du chemin déprécié
T+180j Date Sunset fixée à 90 jours
T+270j Date Sunset arrivée — le point d'accès renvoie 410 Gone avec un lien de migration
Pour les modifications cassantes mineures sur une API épinglée à une version majeure : 6 mois est la médiane. Pour les dépréciations complètes v1 → v2 : 12-24 mois.
4. Le motif de transformation de version
L'architecture interne de Stripe résout le problème « nous avons 50 versions en production » :
Le système utilise des classes de ressources API qui définissent les structures de réponse API actuelles, combinées à des modules de changement de version qui encapsulent les transformations incompatibles avec les versions antérieures. Lors du traitement des réponses, le système remonte dans le temps et applique chaque module de changement de version qu'il trouve jusqu'à atteindre la version cible.
La forme :
// resource : la représentation canonique (la plus récente)
const charge = {
id: 'ch_123',
amount: 1000,
payment_method_details: { card: { brand: 'visa', last4: '4242' } },
}
// Chaque modification cassante est un module transformateur :
const v_2024_03_01 = {
// Supprime le niveau 'card' sous payment_method_details, aplatit les champs
apply(resource) {
return {
...resource,
card_brand: resource.payment_method_details?.card?.brand,
card_last4: resource.payment_method_details?.card?.last4,
}
},
}
const v_2023_10_15 = {
// Renomme 'amount' en 'amount_cents'
apply(resource) {
const { amount, ...rest } = resource
return { ...rest, amount_cents: amount }
},
}
// Pipeline : applique les transformateurs dans l'ordre chronologique inverse
// jusqu'à ce que la version demandée soit atteinte
function transform(resource, requestedVersion) {
const chain = transformers.filter(t => t.date > requestedVersion)
return chain.reduceRight((r, t) => t.apply(r), resource)
}
Les avantages :
- Le code reste moderne — chaque point d'accès est écrit contre le dernier schéma ; pas de
if (version < ...)éparpillé dans la logique métier. - Chaque modification cassante est un fichier — facile à relire, facile à tester isolément, facile à supprimer lorsque le dernier utilisateur la dépasse.
- Télémétrie — vous pouvez compter combien de transformateurs se déclenchent par jour pour planifier les dépréciations en fonction de l'utilisation réelle.
Le coût : la construction du cadre de transformation. Cela vaut le coup si vous avez >3 modifications cassantes par an et >10 000 consommateurs API ; c'est excessif pour une API interne avec <50 clients.
5. API internes vs publiques
La dissymétrie est importante :
| Interne | Publique | |
|---|---|---|
| Déploiement coordonné possible ? | Oui — remplacement atomique | Non — les clients se déploient indépendamment |
| Évolution du schéma | Par ajout + message Slack | Versioning strict + email + tableaux de bord + billet de blog |
| Fenêtre de dépréciation | Jours à semaines | 6-24 mois |
| Nécessité du versioning | Souvent aucun — juste les en-têtes Deprecation | Obligatoire |
| Meilleure stratégie | Évolution par ajout ; version URI seulement en cas de réécriture | En-têtes épinglés à une date (si l'échelle le justifie), URI pour les réécritures majeures |
Le piège : les entreprises traitent toutes les API comme « publiques » et construisent une infrastructure de versioning lourde pour une API utilisée seulement par 3 services internes. Le piège inverse : une API interne divulguée a des consommateurs externes et vous ne pouvez pas réellement la casser.
Défense : faites de « publique » un tag binaire sur le service. Les services publics bénéficient de la cérémonie complète de versioning ; les services internes non, mais bénéficient d'une surveillance agressive de qui les appelle.
Anti-patrons
| Anti-patron | Pourquoi ça fait mal | Solution |
|---|---|---|
Passer de /v1/ à /v2/ pour une seule modification cassante | Oblige les clients à tout migrer pour un seul champ | Version basée sur une date OU évolution par ajout |
Supprimer des points d'accès sans en-tête Sunset | Les clients sont cassés sans avertissement ; les tickets de support explosent | Associez toujours la suppression à Deprecation 90+ jours avant |
Envoyer Deprecation sans Link | Les clients savent que c'est déprécié mais pas quoi faire | Incluez toujours Link: <doc-migration>; rel="deprecation" |
| Exemptions de version codées en dur par client | Devient impossible à maintenir ; on ne peut pas raisonner sur le comportement | Utilisez le motif de transformation ; les « exemptions » deviennent des épingles de version |
| Versionner les API internes comme les publiques | Surcharge de processus sans bénéfice | Évolution par ajout + déploiements atomiques + Slack |
Utiliser le semver (v1.2.3) sur les API REST | Implique un sens qui ne s'applique pas (qu'est-ce qu'un « correctif » pour un schéma JSON ?) | Noms basés sur une date ou uniquement majeure (v1) |
| Date Sunset sans mise en application | Le point d'accès vit éternellement, le code pourrit | Rappel calendrier ; test CI qui échoue 7 jours avant la date Sunset pour forcer l'action |
Novice / Expert / Calendrier
| Novice | Expert | |
|---|---|---|
| Première modification cassante | Passe /v1 à /v2, copie-colle toute l'arborescence | Envisage l'épingle à une date, évalue si le changement est vraiment cassant |
| Informer les clients de la dépréciation | Envoi en masse par email | En-têtes Deprecation + Sunset + Link + email + bannière dans le tableau de bord |
| Maintenir 5 versions | 5 branches if/else dans chaque contrôleur | Modules transformateurs, code central uniquement le plus récent |
| Mise en application de la dépréciation | « On s'en occupera » | Date Sunset dans le CI ; le point d'accès devient 410 Gone à la date |
| Modification d'API interne | Même cérémonie que pour une API publique — lente | Évolution par ajout + déploiement atomique ; réserve les processus lourds pour le public |
Test de calendrier : combien de temps faut-il entre « nous voulons supprimer le champ X » et « le champ X est supprimé en production » ? Une organisation experte a une réponse unique et reproductible (par exemple, 90 jours). Une organisation novice n'a pas de réponse car il n'y a pas de processus.
Tests qualité
Une modification de versioning est livrée lorsque :
- Test : La suppression ou le renommage d'un champ visible de l'extérieur est conditionné par une annonce de dépréciation datant d'au moins N jours (où N est la fenêtre de dépréciation documentée).
- Test : Chaque point d'accès déprécié envoie les en-têtes
Deprecation: @<unix-ts>etSunset: <HTTP-date>, vérifiés dans un test d'intégration. - Test : La valeur
Sunsetn'est jamais antérieure à la valeurDeprecation(selon le DOIT de la RFC 9745). - Test : Chaque annonce de dépréciation inclut un en-tête
Link: <url-doc>; rel="deprecation"ET un document de migration publique qui existe à cette URL (vérifié par HEAD dans le CI). - Test : Les métriques d'utilisation par version existent — vous pouvez répondre à « combien de requêtes sur le chemin v2 hier ? » avant de supprimer v2.
- Test (uniquement pour version épinglée) : Les nouvelles clés API utilisent par défaut la dernière version ; les clés existantes conservent leur épingle à travers les déploiements.
- Manuel : Plan de mise en application de la dépréciation : un rappel calendrier, une vérification CI, ou un 410 Gone automatisé — quelque chose se déclenche lorsque la date arrive.
Pas dans cette compétence
- Évolution de schéma GraphQL (utilisez
graphql-schema-evolution) - Compatibilité filaire gRPC / Protobuf (utilisez
protobuf-evolution-rules) - Versionnage de schéma de base de données (utilisez
zero-downtime-database-migration) - Versionnage semver de bibliothèque / SDK (utilisez
npm-package-versioning) - Évolution de schéma de flux d'événements (utilisez
kafka-schema-registry-design)
Sources
- Stripe : API versioning at Stripe — versions épinglées à une date, motif de transformation
- API Stripe : Idempotent requests / versioning policy
- IETF : RFC 8594 — The Sunset HTTP Header Field
- IETF : RFC 9745 — The Deprecation HTTP Header Field
- IETF : RFC 9651 — Structured Field Values for HTTP (format de date pour l'en-tête Deprecation)
- Roy Fielding : REST APIs must be hypertext-driven — l'argument canonique contre le versioning par URI