11 - Versioning : quand et comment faire évoluer ton API
Ce que tu vas apprendre
- Les trois stratégies de versioning (URL, header, content negotiation)
- Ce qui constitue un breaking change
- Comment deprecier une version sans casser les clients existants
- Le header Sunset et la communication de fin de vie
Prerequisites
Avoir lu l'article sur l'authentification. Comprendre la notion de contrat d'API et de compatibilité ascendante.
J'ai vu un collegue renommer un champ de firstName a first_name en production un vendredi a 17h. "C'est juste un renommage, ca va." Le lundi matin, trois applications clientes etaient cassees. Le versioning existe pour éviter ce genre de situation.
Qu'est-ce qu'un breaking change ?
Avant de parler de versioning, il faut savoir ce qui casse un client :
- Supprimer un champ de la réponse
- Renommer un champ
- Changer le type d'un champ (string vers number)
- Ajouter un champ requis dans le body
- Changer la semantique d'un champ existant
- Modifier un code de status HTTP
Ce qui n'est PAS un breaking change (en général) :
- Ajouter un champ optionnel dans la réponse
- Ajouter un query parameter optionnel
- Ajouter un nouvel endpoint
Si tes clients sont tolerants (ils ignorent les champs inconnus), tu peux faire évoluer l'API longtemps sans nouvelle version.
Stratégie 1 : URL path
La plus simple, la plus repandue :
GET /v1/users/42
GET /v2/users/42
Avantages : visible, facile a router, facile a tester. Tu peux pointer un client vers /v1 et un autre vers /v2 sans configuration speciale.
Inconvenients : ce n'est pas tres REST (l'URL devrait identifier la ressource, pas la version). Et tu te retrouves avec deux arborescences de routes a maintenir.
typescript// Express
import v1Router from "./routes/v1";
import v2Router from "./routes/v2";
app.use("/v1", v1Router);
app.use("/v2", v2Router);
C'est ce que font Stripe, GitHub, Google. Si ca leur suffit, ca te suffira probablement aussi.
Stratégie 2 : header custom
GET /users/42
Accept-Version: 2
L'URL reste propre. La version est dans les headers. C'est ce que fait Heroku avec son API.
typescriptapp.get("/users/:id", (req, res) => {
const version = parseInt(req.headers["accept-version"] as string) || 1;
if (version >= 2) {
return res.json(formatUserV2(user));
}
return res.json(formatUserV1(user));
});
Avantage : l'URL identifié la ressource, le header specifie la representation. Plus fidele a REST.
Inconvenient : plus dur a tester (tu ne peux pas juste coller l'URL dans un navigateur), moins visible dans les logs.
Stratégie 3 : content negotiation
GET /users/42
Accept: application/vnd.myapi+json;version=2
La version est dans le type de media. C'est la plus "pure" en termes REST. GitHub l'utilise aussi (en plus du path).
En pratique, je ne recommande pas cette approche sauf si tu construis une API publique avec des milliers de consommateurs et que tu as une équipe dédiée au developer experience. C'est trop complique pour le gain.
Mon conseil
Utilise le path /v1/... par défaut. C'est simple, c'est universel, tout le monde comprend. Tu n'as pas besoin de la purete theorique, tu as besoin que ca marche et que ce soit maintenable.
Un truc que j'ai appris : ne versionne pas des le jour 1. Commence sans version dans l'URL. Quand tu as ton premier breaking change, ajoute /v1 (l'API actuelle) et /v2 (la nouvelle). Tu ne sais pas combien de versions tu auras. Peut-etre une seule.
La deprecation
Quand tu lances une v2, la v1 ne disparaît pas du jour au lendemain. Tu as des clients qui l'utilisent. Voici la marche a suivre :
- Annonce : previens tes clients (email, changelog, dashboard) que la v1 sera dépréciée
- Header Sunset : ajoute un header qui indique la date de fin de vie
- Migration guide : documente les changements entre v1 et v2
- Monitoring : mesure le trafic sur v1 pour savoir qui n'a pas migre
- Coupure : quand le trafic est proche de zero, coupe la v1
Le header Sunset
Defini dans la RFC 8594, le header Sunset indique quand un endpoint sera retire :
HTTP/1.1 200 OK
Sunset: Sat, 01 Nov 2026 00:00:00 GMT
Deprecation: true
Link: <https://api.paltemps.fr/v2/users>; rel="successor-version"
typescriptapp.use("/v1", (req, res, next) => {
res.set("Sunset", "Sat, 01 Nov 2026 00:00:00 GMT");
res.set("Deprecation", "true");
res.set("Link", '</v2>; rel="successor-version"');
next();
});
Les clients automatises peuvent parser ce header et alerter les équipes. C'est beaucoup plus fiable qu'un email que personne ne lira.
Versionner au niveau du serializer
Plutot que de dupliquer toute la logique métier entre v1 et v2, versionne uniquement la couche de sérialisation :
typescript// La logique metier ne change pas
const user = await userService.findById(id);
// Seul le format de reponse change
function serializeUserV1(user: User) {
return { id: user.id, firstName: user.firstName, lastName: user.lastName };
}
function serializeUserV2(user: User) {
return { id: user.id, name: { first: user.firstName, last: user.lastName } };
}
La couche service reste unique. Seule la representation change. Ca évité la duplication de code et les bugs qui vont avec.
Résumé
- Un breaking change, c'est tout ce qui peut casser un client existant (suppression, renommage, changement de type)
- Le versioning par URL path (
/v1/...) est le plus simple et le plus repandu - Ne versionne pas avant d'en avoir besoin
- Utilise le header
Sunsetpour communiquer les dates de deprecation - Versionne la sérialisation, pas la logique métier
Article précédent : Authentification Article suivant : CORS