Architecture
Versionner une API REST sans toucher aux URL, avec Spring 7
Pourquoi j'ai choisi la négociation de contenu plutôt qu'un /v1 dans l'URL, et comment Spring Framework 7 le met en place en trois propriétés.
Le problème : des clients qui appellent l'API en dur
Je maintiens une petite API REST personnelle sur Spring Boot 4, donc Spring Framework 7. Plusieurs clients l'appellent directement : des agents IA, des scripts, quelques intégrations. Ils appellent des routes comme /api/shorten ou /api/random telles quelles.
Aucun endpoint n'était versionné. Tant que rien ne change, ça va. Le jour où je veux changer le format d'une réponse, par exemple renvoyer un objet JSON au lieu d'une simple chaîne, je casse tous ces clients d'un coup.
Il me fallait donc un moyen de livrer une version 2 sans casser la version 1. La vraie question : comment un client choisit-il la version qu'il reçoit ?
Trois options sur la table
Le préfixe dans l'URL (/api/v1/...) est le réflexe le plus courant. Il est explicite et facile à router. Mais il casse tous les appels existants, sauf à maintenir des alias pour chaque route. Et il pousse vite à dupliquer les contrôleurs et les DTO dans des paquets v1 et v2.
Un en-tête maison (X-API-Version) ou un paramètre de requête (?version=1) garde les URL intactes. C'est mieux. Mais on invente un mécanisme alors qu'HTTP en a déjà un.
La négociation de contenu utilise justement ce mécanisme standard. Le client demande une représentation précise dans l'en-tête Accept :
httpAccept: application/json;version=1
L'URL ne bouge pas. La version fait partie du type de média demandé, ce qui est exactement son rôle.
Le choix, et pourquoi il ne coûte presque rien
J'ai retenu la négociation de contenu pour une raison simple : aucun client n'a à migrer. Une requête sans version reçoit la version 1, comme avant.
La bonne surprise, c'est Spring Framework 7. Le versioning d'API y est intégré nativement. Pas d'intercepteur à écrire, pas de constantes de chemin. Trois lignes dans application.properties :
propertiesspring.mvc.apiversion.supported=1.0 spring.mvc.apiversion.default=1.0 spring.mvc.apiversion.use.media-type-parameter[application/json]=version
La première liste les versions acceptées. La deuxième sert la 1.0 quand le client ne précise rien. La troisième dit à Spring de lire la version dans le paramètre version du type application/json.
Les pages HTML (Thymeleaf) ne sont pas concernées : la règle ne porte que sur application/json.
Ce que voit le client
Côté client, une requête sans version passe normalement :
bashcurl -H "Accept: application/json" "https://api.example.com/api/random?min=1&max=6" # {"value":6,"min":1,"max":6,"timestamp":"..."}
Une version inconnue renvoie une erreur 400, au même format que les autres erreurs de l'API :
bashcurl -H "Accept: application/json;version=9" "https://api.example.com/api/random?min=1&max=6" # {"status":400,"error":"Bad Request","message":"Invalid API version: '9.0.0'.","path":"/api/random"}
Deux détails. D'abord, c'est une 400 et pas une 406 : Spring lève une InvalidApiVersionException, que mon gestionnaire d'erreurs global transforme en réponse standard. Ensuite, Spring normalise la version : 9 devient 9.0.0 dans le message. Bon à savoir avant d'écrire des tests sur le texte exact.
À quoi ressemblera la version 2
Quand un changement cassant arrivera, j'ajouterai une méthode à côté de l'existante, dans le même contrôleur :
kotlin@GetMapping("/shorten") // version 1, implicite fun shorten(@RequestParam url: String): String = ... @GetMapping("/shorten", version = "2") // Accept: application/json;version=2 fun shortenV2(@RequestParam url: String): ShortenResponseV2 = ...
Pas de paquet v2, pas de contrôleur dupliqué. Il faudra seulement ajouter 2.0 à spring.mvc.apiversion.supported.
Le tester
Un test @WebMvcTest couvre le comportement, sur un endpoint JSON et une page HTML. Cinq cas suffisent :
- sans version, la requête est servie en version 1
- avec
version=1, elle est servie - avec une version inconnue (
version=9), réponse 400 - avec une version mal formée (
version=abc), réponse 400 - une page HTML répond normalement
kotlin@Test fun unsupportedVersionReturns400() { mockMvc.perform(get("/api/random").accept("application/json;version=9")) .andExpect(status().isBadRequest) .andExpect(jsonPath("$.status").value(400)) }
Ce qui reste à faire le jour de la v2
J'ai volontairement repoussé trois sujets, parce que rien n'en a besoin tant qu'il n'existe qu'une version.
Le cache, d'abord. Mon API est derrière un CDN. Avec deux versions sur la même URL, un cache partagé doit savoir que la réponse dépend de l'en-tête Accept. Spring n'ajoute pas Vary: Accept tout seul : il faudra le faire.
La dépréciation, ensuite. Spring 7 sait annoncer la fin d'une version avec les en-têtes Deprecation, Sunset et Link. Utile le jour où la version 1 devra partir.
OpenAPI Generator, enfin. Il gère mal plusieurs types de média pour un même chemin (issue #11000 du projet). Sans spécification publiée pour l'instant, ça ne me gêne pas. Ce sera à surveiller si j'en publie une.
Ce que j'en retiens
Le bilan est très positif pour un coût minime : trois propriétés, aucune migration, aucune URL dupliquée. Le versioning est en place avant même d'en avoir besoin, et il ne gêne personne.
Il y a deux contreparties à connaître. La version n'apparaît plus dans les URL ni dans les logs d'accès : si j'en ai besoin pour diagnostiquer, il faudra la journaliser explicitement. Et la négociation de contenu demande un peu plus de rigueur côté cache dès qu'une deuxième version existe.
Si votre API a déjà des clients et que vous êtes sur Spring Boot 4, c'est à mon avis le choix le plus simple aujourd'hui. Tout est consigné dans un ADR du projet, avec les options écartées, pour que la décision reste lisible dans six mois.
// À lire ensuite
Business
Output-based vs Time & Material : pourquoi on a tué le T&M chez Abbeal.
78 % du portfolio Abbeal en Output-based en 2026. Marge brute +18 pts, NPS +24, durée moyenne mission ×1,7. Comment on opère et 3 conditions de succès.
11 min
Talent
Comment construire une équipe d'ingénierie senior à travers l'Asie, l'Europe et l'Amérique du Nord
Le playbook pour assembler une équipe d'ingénierie senior qui opère sur trois continents — Asie, Europe et Amérique du Nord. Le modèle Abbeal à trois hubs : Paris · Montréal · Tokyo.
7 min
IA
Comment j'ai automatisé une journée de CEO d'ESN avec Claude (et ce que vous pouvez en tirer).
30 workflows orchestrés sur Notion + BoondManager + Google Workspace + LinkedIn + Apollo + Calendly + Tactiq, sans nouveau SaaS. 4 piliers : commercial multicanal anti-doublon, recrutement 48h, inbound SEO/LinkedIn/citations IA, productivité dirigeant. Zéro lead perdu en 6 mois, 15 min/jour vs 3-4h avant.
7 min
IA
Agents IA en production : éviter le théâtre de démo.
Fiabilité, coûts, sécurité, évaluation. Sept patterns qu'on utilise vraiment chez nos clients.
9 min
GreenOps
GreenOps : sept leviers qui coupent 30 % de votre facture cloud.
Sans sacrifier la performance. Cas concrets : −30 % sur la facture, mêmes SLOs.
6 min
