📍 Guide du parcours : Concevoir une API REST — le guide de conception. Cet article approfondit une brique du parcours ; pour la vue d’ensemble, commencez par le guide.
Faire évoluer une API sans casser ceux qui l’utilisent
Une API qui réussit a un problème de riche : des clients. Le jour où la médiathèque décide qu’un livre peut avoir plusieurs auteurs, il faut remplacer le champ auteur (un objet) par auteurs (un tableau). Côté serveur, c’est cinq minutes de travail. Côté écosystème, c’est une bombe : chaque application mobile, chaque script partenaire qui lit livre.auteur.nom va planter du jour au lendemain. Vous ne contrôlez pas ces clients, parfois vous ignorez même qu’ils existent. La question n’est donc pas « comment changer l’API ? » mais « comment la changer sans casser ce qui tourne déjà ? ».
Ce tutoriel répond à cette question. Vous apprendrez à distinguer un changement anodin d’un changement cassant, à introduire une version 2 qui cohabite avec la 1, et à éteindre proprement une ancienne version avec les en-têtes normalisés Deprecation et Sunset — pour que la disparition d’un endpoint soit annoncée, datée, et jamais subie.
🎯 Ce que vous allez apprendre
- Reconnaître ce qui constitue un changement cassant et ce qui n’en est pas un.
- Comparer le versionnage par URI et par type de média, et choisir en connaissance de cause.
- Faire cohabiter deux versions de l’API sur le même serveur.
- Annoncer une dépréciation avec l’en-tête
Deprecation(RFC 9745) et une date d’extinction avecSunset(RFC 8594). - Dérouler une migration qui ne prend personne par surprise.
🛠️ Ce que vous allez construire
Deux versions vivantes de l’API Catalogue : /v1/livres qui renvoie auteur (objet unique), et /v2/livres qui renvoie auteurs (tableau). La version 1 répondra toujours, mais ses réponses porteront les en-têtes qui annoncent sa dépréciation et la date à laquelle elle cessera de fonctionner.
Prérequis
- Un serveur Express qui sert l’API Catalogue.
- La notion de représentation d’une ressource, vue dans le guide du parcours.
- De quoi inspecter les en-têtes de réponse (
curl -Iou l’onglet réseau du navigateur). - ⏱️ Temps estimé : environ 45 minutes.
Étape 1 — Tous les changements ne se valent pas
Avant de parler de version, il faut savoir quand on en a besoin. La règle est nette : on ne crée une nouvelle version que pour un changement cassant, c’est-à-dire un changement qui ferait échouer un client existant correctement écrit. Tout le reste se fait en place.
| Changement cassant (nouvelle version) | Changement compatible (en place) |
|---|---|
| Supprimer ou renommer un champ | Ajouter un nouveau champ optionnel |
Changer le type d’un champ (auteur objet → tableau) |
Ajouter un nouvel endpoint |
| Rendre obligatoire un paramètre jusque-là optionnel | Ajouter une valeur possible à une énumération de sortie |
| Changer le format d’un identifiant ou d’une date | Ajouter un en-tête de réponse |
| Modifier un code de statut renvoyé | Corriger un bogue sans changer le contrat |
La colonne de droite repose sur un principe précieux, le principe de robustesse : un client bien écrit ignore les champs qu’il ne connaît pas. C’est pourquoi ajouter un champ ne casse rien — les anciens clients ne le voient simplement pas. Concevez vos clients dans cet esprit (ignorer l’inconnu plutôt que planter dessus), et vous réduisez d’autant le besoin de versionner. Beaucoup d’évolutions qu’on croit cassantes ne le sont pas si on les pense en additif.
Étape 2 — Où loger la version ?
Deux approches dominent, et le débat entre elles est ancien. Le versionnage par URI place la version dans le chemin : /v1/livres, /v2/livres. C’est la plus répandue, parce qu’elle est visible, triviale à tester dans un navigateur, et sans ambiguïté : on voit la version dans chaque URL, chaque log, chaque exemple de doc.
GET /v1/livres/42 → { "auteur": { "nom": "Mariama Bâ" } }
GET /v2/livres/42 → { "auteurs": [ { "nom": "Mariama Bâ" } ] }
Le versionnage par type de média garde une URL unique et met la version dans l’en-tête Accept :
GET /livres/42
Accept: application/vnd.catalogue.v2+json
Cette seconde approche est jugée plus « pure » par les puristes de REST : l’URL identifie la ressource, et la version n’est qu’une question de représentation négociée — exactement le rôle de l’en-tête Accept vu dans le guide. En contrepartie, elle est plus difficile à tester (on ne peut pas juste coller l’URL dans un navigateur) et moins lisible dans les journaux. Pour la majorité des API, surtout publiques, le versionnage par URI gagne par sa simplicité ; on retiendra le type de média quand la pureté du modèle de ressources prime. L’essentiel, là encore, est de choisir une approche et de ne jamais en changer.
Étape 3 — Faire cohabiter deux versions
Adopter le versionnage par URI, c’est faire vivre deux jeux de routes sur le même serveur. Express rend cela limpide avec les routeurs montés sur un préfixe : chaque version a son routeur, et le code partagé reste mutualisé.
const express = require('express');
const app = express();
const v1 = express.Router();
const v2 = express.Router();
// v1 : le livre a UN auteur (ancien contrat)
v1.get('/livres/:id', (req, res) => {
const livre = trouverLivre(req.params.id);
res.json({ id: livre.id, titre: livre.titre, auteur: livre.auteurs[0] });
});
// v2 : le livre a PLUSIEURS auteurs (nouveau contrat)
v2.get('/livres/:id', (req, res) => {
const livre = trouverLivre(req.params.id);
res.json({ id: livre.id, titre: livre.titre, auteurs: livre.auteurs });
});
app.use('/v1', v1);
app.use('/v2', v2);
La donnée sous-jacente est la même (un livre a toujours une liste d’auteurs en base) ; seules les représentations diffèrent. La v1 expose le premier auteur sous l’ancien nom de champ, la v2 expose la liste complète. Aucun client n’est cassé : les anciens continuent d’appeler /v1, les nouveaux passent à /v2 à leur rythme.
✅ Point d’étape —
GET /v1/livres/42renvoie un champauteur;GET /v2/livres/42renvoie un champauteursqui est un tableau. Les deux répondent, en même temps, depuis le même serveur.
Étape 4 — Annoncer la dépréciation avec l’en-tête Deprecation
Faire cohabiter deux versions indéfiniment a un coût : chaque correction, chaque audit de sécurité doit couvrir les deux. À un moment, on veut éteindre la v1. Mais on ne débranche pas une API du jour au lendemain : on annonce, longtemps à l’avance, par un canal que les machines comprennent. C’est le rôle de l’en-tête Deprecation, normalisé par la RFC 9745 (publiée en mars 2025).
Sa valeur est une date au format « champ structuré » de HTTP : un arobase suivi d’un horodatage Unix. Deprecation: @1748736000 annonce que la ressource est dépréciée depuis le 1er juin 2025. Ajoutez un middleware sur la v1 :
v1.use((req, res, next) => {
// @ + horodatage Unix = date au format champ structuré (RFC 9745)
res.set('Deprecation', '@1748736000');
// Un lien vers la doc de migration (relation "deprecation")
res.set('Link', '<https://catalogue.exemple.org/migration-v2>; rel="deprecation"');
next();
});
Déclarez ce middleware avant les routes v1.get(...) de l’étape 3 : Express exécute les couches dans leur ordre de déclaration, donc un middleware ajouté après les routes ne s’appliquerait jamais à leurs réponses. L’en-tête Deprecation ne change rien au fonctionnement : la v1 répond exactement comme avant. Il informe, c’est tout. Un client outillé peut détecter cet en-tête dans ses journaux et alerter son équipe « cette API que nous utilisons est dépréciée », bien avant que quoi que ce soit ne casse. Le lien rel="deprecation" pointe vers le guide de migration : on dit à la fois que c’est déprécié et comment migrer.
✅ Point d’étape —
curl -I http://localhost:3000/v1/livres/42doit montrer un en-têteDeprecation: @1748736000et un en-têteLinkavecrel="deprecation". La v2, elle, ne porte aucun de ces en-têtes.
Étape 5 — Dater l’extinction avec Sunset
« Déprécié » dit « ne comptez plus là-dessus » ; il ne dit pas quand ça s’arrêtera. Pour cela, on ajoute l’en-tête Sunset, défini par la RFC 8594 (2019). Il indique la date à partir de laquelle la ressource cessera de répondre. Contrairement à Deprecation, Sunset utilise le format de date classique de HTTP (date dite « IMF », en GMT) :
v1.use((req, res, next) => {
res.set('Deprecation', '@1748736000'); // dépréciée depuis…
res.set('Sunset', 'Wed, 31 Dec 2025 23:59:59 GMT'); // éteinte le…
res.set('Link', '<https://catalogue.exemple.org/migration-v2>; rel="deprecation"');
next();
});
Une contrainte de bon sens, imposée par la norme : la date de Sunset ne doit jamais être antérieure à celle de Deprecation — on n’éteint pas une chose avant de l’avoir annoncée. L’écart entre les deux dates, c’est votre période de migration : ici, six mois pour que les clients passent en v2. Cette fenêtre se communique aussi hors bande (courriel aux partenaires, page de statut), mais l’avoir dans les en-têtes la rend exploitable automatiquement.
✅ Point d’étape final — Les réponses de la v1 portent désormais
Deprecation,Sunsetet unLinkde migration. Un client a tout ce qu’il faut, par programme, pour savoir qu’il doit migrer et avant quelle date. La v1 fonctionne toujours : rien n’est cassé, tout est annoncé.
Étape 6 — Dérouler la migration
Les en-têtes posent le cadre ; la migration est un processus. Une extinction réussie suit à peu près toujours les mêmes phases :
- Publier la v2 et sa documentation, en laissant la v1 intacte. Les deux cohabitent, personne n’est pressé.
- Annoncer la dépréciation : en-têtes
Deprecation/Sunset, guide de migration, message aux partenaires connus. - Mesurer : instrumentez la v1 pour savoir qui l’appelle encore et combien. Sans cette mesure, on éteint à l’aveugle.
- Relancer les retardataires à l’approche de la date de
Sunset, en s’appuyant sur les chiffres de trafic. - Éteindre : à la date annoncée, la v1 renvoie un
410 Gone(la ressource a existé mais n’existe plus), avec un message renvoyant vers la v2.
Le 410 Gone est plus précis qu’un 404 pour une version éteinte : il dit « ce n’est pas une erreur de votre part, cet endpoint a été délibérément retiré ». Versionner proprement, ce n’est pas seulement créer des v2 : c’est savoir refermer les v1 sans laisser personne dans le noir.
Deux pratiques rendent ce processus nettement plus humain pour ceux qui consomment l’API. La première est de tenir un journal des changements (changelog) public et daté : chaque entrée dit ce qui a changé, si c’est cassant, et vers quoi migrer. C’est le premier endroit où un développeur intégrateur va chercher quand quelque chose se comporte autrement ; un changelog clair évite des heures de débogage à l’aveugle. La seconde est d’annoncer à l’avance une politique de versions explicite — par exemple « nous maintenons la version courante et la précédente, et nous laissons au moins six mois entre la dépréciation et l’extinction ». Une politique écrite transforme l’évolution de l’API d’une suite de surprises en un contrat prévisible : les intégrateurs peuvent planifier leur propre travail au lieu de réagir dans l’urgence. La technique (en-têtes, routes, codes de statut) n’est que la moitié du versionnage ; la communication en est l’autre moitié, et c’est souvent elle qui décide si une migration se passe bien.
🐞 Pièges fréquents
| Symptôme | Cause probable | Correctif |
|---|---|---|
| Des clients cassent après un simple ajout de champ | Clients qui plantent sur un champ inconnu | Côté client : ignorer l’inconnu (principe de robustesse) |
| On multiplie les versions pour des changements anodins | Tout changement traité comme cassant | Versionner uniquement les changements réellement cassants |
Deprecation: 2025-06-01 ignoré par les outils |
Mauvais format (RFC 9745 attend @ + Unix) |
Écrire Deprecation: @1748736000 |
Sunset antérieur à Deprecation |
Dates incohérentes | Sunset toujours ≥ Deprecation |
| On éteint la v1 et le trafic s’effondre | Aucune mesure de qui utilisait encore la v1 | Instrumenter avant d’éteindre |
🌍 Réalités du terrain
La durée de la fenêtre de migration n’est pas qu’une question technique : elle dépend de qui sont vos clients. Quand une partie des intégrations est portée par des applications mobiles qui se mettent à jour lentement — parce que l’utilisateur final repousse les mises à jour pour économiser ses données, ou parce que le réseau rend le téléchargement pénible — une fenêtre de quelques semaines est irréaliste. Compter plusieurs mois entre Deprecation et Sunset est souvent prudent, et c’est précisément ce que les en-têtes permettent de communiquer sans ambiguïté.
Côté coût, maintenir deux versions en parallèle double une partie du travail : chaque correctif de sécurité, chaque test doit couvrir la v1 et la v2. C’est une raison concrète de ne pas laisser traîner une version dépréciée éternellement : la mesure du trafic résiduel sert aussi à justifier, chiffres en main, le moment où l’on peut enfin retirer l’ancien code et alléger la maintenance.
✅ Récapitulatif
Vous savez maintenant faire évoluer une API sans trahir ses utilisateurs. Vous distinguez les changements cassants (qui exigent une nouvelle version) des changements additifs (qui se font en place grâce au principe de robustesse). Vous avez fait cohabiter /v1 et /v2 sur un même serveur, puis annoncé l’extinction de la v1 avec les en-têtes Deprecation (RFC 9745) et Sunset (RFC 8594), accompagnés d’un lien de migration. Enfin, vous connaissez le déroulé d’une migration — publier, annoncer, mesurer, relancer, éteindre en 410 Gone. Une bonne API ne se fige pas : elle change en prévenant.
🧾 Aide-mémoire
| Élément | Rôle |
|---|---|
/v1/…, /v2/… |
Versionnage par URI (le plus répandu) |
Accept: application/vnd.catalogue.v2+json |
Versionnage par type de média |
Deprecation: @1748736000 |
Annonce la dépréciation (RFC 9745) |
Sunset: <date GMT> |
Date d’extinction (RFC 8594) |
Link: …; rel="deprecation" |
Vers le guide de migration |
410 Gone |
Version délibérément retirée |
💪 À vous de jouer
À la date de Sunset, faites répondre la v1 par un 410 Gone avec un corps au format « problème » qui renvoie vers la v2.
Voir une solution
v1.use((req, res) => { // catch-all : couvre toute la v1 retirée
res.status(410)
.type('application/problem+json')
.json({
type: 'https://catalogue.exemple.org/erreurs/version-retiree',
title: 'La version 1 de l\'API a été retirée',
status: 410,
detail: 'Migrez vers /v2. Voir https://catalogue.exemple.org/migration-v2'
});
});
Le 410 est définitif et explicite ; le corps problem+json (vu dans le guide) donne au client de quoi rediriger l’utilisateur ou son code vers la v2.
Tutoriels frères
- Documenter une API REST avec OpenAPI — maintenir un contrat par version.
- Pagination, filtres et tri — faire évoluer les paramètres d’une liste sans casser les clients.
Pour aller plus loin
- 🔝 Retour au guide : Concevoir une API REST.
- RFC 9745 — The Deprecation HTTP Response Header Field : rfc-editor.org/rfc/rfc9745.html.
- RFC 8594 — The Sunset HTTP Header Field : rfc-editor.org/rfc/rfc8594.html.
FAQ
Combien de versions faut-il maintenir en parallèle ? Le moins possible — idéalement deux : la version courante et la précédente en cours de dépréciation. Au-delà, le coût de maintenance et de tests explose. Une politique claire (« nous gardons N-1 ») fixe les attentes des clients.
Faut-il versionner dès la version 1 ? Oui. Démarrer directement en /v1 coûte zéro et vous évite, le jour de la première rupture, d’avoir à introduire le versionnage en même temps qu’un changement cassant — deux migrations en une.
Un changement de comportement sans changement de format est-il cassant ? Souvent, oui. Si un endpoint qui renvoyait tous les livres se met à n’en renvoyer que les disponibles, le format n’a pas bougé mais le contrat implicite, si. Dans le doute, traitez un changement de sémantique observable comme cassant.
Que renvoyer pendant la dépréciation : une erreur ou un avertissement ? Surtout pas une erreur : la ressource dépréciée doit continuer à fonctionner normalement jusqu’au Sunset. On informe via les en-têtes, on ne pénalise pas. Pénaliser avant l’heure annoncée, c’est casser sans prévenir.
Mots-clés : versionnage API, dépréciation, breaking change, Deprecation header RFC 9745, Sunset RFC 8594, migration API, 410 Gone, versionnage par URI.