Chapitre 16 — Évolution et versioning d’API#
Une API publiée est un contrat. Modifier ce contrat sans en avertir les consommateurs, c’est casser leurs applications à distance — parfois en production, parfois en silence. Ce chapitre couvre les stratégies pour faire évoluer une API de manière prévisible : distinguer ce qui casse de ce qui ne casse pas, choisir une stratégie de versioning adaptée, gérer la déprécation, et faciliter la migration des clients.
Backward compatibility — ce qui casse et ce qui ne casse pas#
La compatibilité ascendante (backward compatibility) est la propriété d’un changement qui garantit que les clients existants continuent de fonctionner sans modification. La règle de base : un changement est breaking s’il oblige un client existant à modifier son code.
Changements non-breaking (additive)#
Un changement additif ne retire rien et n’altère pas la sémantique existante. Les clients ignorent ce qu’ils ne connaissent pas.
Ajout d’un champ dans la réponse JSON : un client qui désérialise dans un objet partiel ne voit pas le nouveau champ. Pas de rupture.
Ajout d’un endpoint : les clients qui n’appellent pas ce endpoint ne sont pas affectés.
Ajout d’un paramètre optionnel : les clients qui ne le fournissent pas obtiennent le comportement par défaut.
Ajout d’une valeur dans un enum de réponse : les clients tolérants ignorent les valeurs inconnues.
Assouplissement d’une contrainte de validation : accepter un champ autrefois obligatoire comme optionnel.
Principe de tolérance
Les clients bien conçus ignorent les champs inconnus dans les réponses JSON. En Python, pydantic avec model_config = ConfigDict(extra='ignore') (défaut) absorbe silencieusement les nouveaux champs serveur.
Changements breaking#
Suppression d’un champ ou d’un endpoint : les clients qui le lisent ou l’appellent échouent.
Renommage d’un champ : équivalent à supprimer l’ancien et ajouter le nouveau.
Changement de type : transformer
"age": 30en"age": "30"casse la désérialisation typée.Changement de sémantique : modifier la signification d’un champ sans changer son nom (ex :
status: "active"qui signifie maintenant quelque chose de différent).Rendre un champ optionnel obligatoire : les clients qui ne l’envoyaient pas échouent à la validation.
Changement de format de date : passer de
ISO 8601à un timestamp Unix casse le parsing.Modification du code HTTP de succès : remplacer
200par201peut tromper les clients qui testent l’égalité stricte.
Matrice de risque
Classez chaque changement envisagé en trois catégories : safe (peut merger sans version bump), additive (nécessite communication mais pas de version bump MAJOR), breaking (nécessite un bump MAJOR et une période de coexistence).
Loi de Postel revisitée — Tolerant Reader#
Jon Postel a formulé en 1980 la règle de robustesse pour TCP/IP : « Be conservative in what you send, be liberal in what you accept. » Cette règle s’applique directement aux APIs.
Interprétation moderne#
Côté serveur : envoyez des réponses strictement conformes au contrat. N’ajoutez pas de champs non documentés, n’envoyez pas de types ambigus. La sortie doit être prévisible.
Côté client : soyez tolérant envers les extensions que vous ne comprenez pas. N’échouez pas si la réponse contient un champ supplémentaire. N’échouez pas si un enum contient une valeur inconnue — traitez-la comme une valeur par défaut.
Pattern Tolerant Reader#
Le pattern Tolerant Reader (Martin Fowler, 2011) formalise cette tolérance côté client :
# Tolerant Reader : ne récupérer que ce dont on a besoin
def parse_user(response_json: dict) -> User:
return User(
id=response_json["id"], # obligatoire
name=response_json["name"], # obligatoire
email=response_json.get("email"), # optionnel
# role inconnu -> ignoré silencieusement
)
Limite du principe
La libéralité en entrée peut devenir un vecteur de sécurité si elle accepte des payloads malformés. La loi de Postel s’applique au niveau protocolaire, pas au niveau sécurité. Validez toujours les entrées, même en étant « libéral » sur les champs supérieurs.
Consumer-Driven Contracts#
Les consumer-driven contracts (Pact, Spring Cloud Contract) inversent la dépendance : c’est le consommateur qui définit ce qu’il attend du fournisseur. Le fournisseur doit vérifier qu’il satisfait ces contrats avant chaque déploiement. Cela rend les breaking changes visibles avant qu’ils n’atteignent la production.
Semantic versioning pour les APIs#
Le semantic versioning (SemVer, semver.org) est un schéma MAJOR.MINOR.PATCH avec des règles strictes sur quand incrémenter chaque partie.
Règles SemVer appliquées aux APIs#
PATCH (ex : 1.0.1 → 1.0.2) : corrections de bugs qui ne changent pas le comportement documenté. Correction d’une validation trop stricte, fix d’un bug de calcul, amélioration de performance.
MINOR (ex : 1.0.0 → 1.1.0) : ajouts backward-compatible. Nouveaux endpoints, nouveaux champs optionnels, nouveaux paramètres facultatifs, nouveaux codes d’erreur documentés.
MAJOR (ex : 1.x.x → 2.0.0) : changements breaking. Suppression de ressources, renommage, changement de type, modification de sémantique.
Version 0.x.x
En SemVer, les versions 0.x.x sont en développement. Tout peut changer sans préavis. Une API publique devrait passer en 1.0.0 dès qu’elle est utilisée en production par des tiers, même en interne.
Pre-release et build metadata#
2.0.0-alpha.1: pre-release, pas de garantie de stabilité2.0.0-beta.3: feature-complete, en cours de stabilisation2.0.0-rc.1: release candidate, corrections uniquement2.0.0+build.20240101: metadata de build, ignoré dans la comparaison de versions
Versioning dans la pratique#
La version sémantique de l’API n’est pas forcément exposée directement dans les URLs (voir section 4). Elle sert principalement à communiquer l’impact des changements dans le changelog et aux outils de gestion de dépendances.
Stratégies de versioning#
Il existe trois approches principales pour exposer la version d’une API aux clients.
URI versioning#
La version est encodée dans le chemin de l’URL :
GET /api/v1/users
GET /api/v2/users
Avantages : visible immédiatement, facile à router côté load balancer, facile à tester dans un navigateur, simple à comprendre.
Inconvénients : viole la contrainte REST qui dit qu’une URI identifie une ressource (pas sa version), encourage la prolifération de copies de code, rend les URLs non-canoniques (la « vraie » ressource /users n’a pas d’URI stable).
Header versioning#
La version est transportée dans un header HTTP :
GET /api/users HTTP/1.1
API-Version: 2
ou via un header de date (style Stripe) :
GET /api/users HTTP/1.1
Stripe-Version: 2024-01-15
Avantages : URLs stables et canoniques, conforme à l’esprit REST, permet des dates de version précises (chaque déploiement peut être une « version »).
Inconvénients : invisible dans les URLs (moins découvrable), nécessite une configuration supplémentaire dans les clients HTTP, la mise en cache Vary sur le header peut être mal supportée.
Content negotiation#
La version est encodée dans le type MIME :
GET /api/users HTTP/1.1
Accept: application/vnd.myapi.v2+json
Avantages : sémantique HTTP pure, permet une véritable négociation de version.
Inconvénients : complexe à implémenter et à documenter, peu intuitif pour les développeurs, support inégal dans les outils.
Recommandations#
Recommandation pratique
Pour les APIs publiques ou internes avec de nombreux consommateurs : URI versioning. Simple, explicite, bien supporté par tous les outils. Pour les APIs internes avec des clients contrôlés : header versioning. Pour les APIs qui évoluent fréquemment avec des clients sophistiqués : le versioning par date (style Stripe) offre une granularité maximale.
# FastAPI — coexistence v1 et v2
from fastapi import FastAPI
from fastapi.routing import APIRouter
app = FastAPI()
router_v1 = APIRouter(prefix="/api/v1")
router_v2 = APIRouter(prefix="/api/v2")
@router_v1.get("/users/{user_id}")
async def get_user_v1(user_id: int):
# Schéma v1 : champ "fullname" (snake_case)
return {"id": user_id, "fullname": "Alice Dupont", "email": "alice@example.com"}
@router_v2.get("/users/{user_id}")
async def get_user_v2(user_id: int):
# Schéma v2 : champs séparés, ajout de "created_at"
return {
"id": user_id,
"first_name": "Alice",
"last_name": "Dupont",
"email": "alice@example.com",
"created_at": "2024-01-15T10:30:00Z",
}
app.include_router(router_v1)
app.include_router(router_v2)
Déprécation#
Déprécation et suppression sont deux événements distincts. La déprécation annonce l’intention de supprimer ; la suppression retire effectivement. Entre les deux, une période de coexistence permet aux clients de migrer.
Headers de déprécation standardisés#
RFC 8594 — Sunset header : date à laquelle le endpoint cessera de fonctionner.
HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Deprecation: Tue, 01 Jul 2025 00:00:00 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
Le header Sunset est standardisé (RFC 8594). Le header Deprecation est dans le draft IETF draft-ietf-httpapi-deprecation-header. Le header Link avec rel="successor-version" pointe vers le remplaçant.
Middleware Sunset automatique#
from fastapi import FastAPI, Request, Response
from datetime import datetime, timezone
app = FastAPI()
DEPRECATED_PATHS = {
"/api/v1/users": {
"sunset": "Sat, 31 Dec 2025 23:59:59 GMT",
"successor": "https://api.example.com/api/v2/users",
"deprecation": "Tue, 01 Jul 2025 00:00:00 GMT",
}
}
@app.middleware("http")
async def add_deprecation_headers(request: Request, call_next):
response: Response = await call_next(request)
for path_prefix, meta in DEPRECATED_PATHS.items():
if request.url.path.startswith(path_prefix):
response.headers["Sunset"] = meta["sunset"]
response.headers["Deprecation"] = meta["deprecation"]
response.headers["Link"] = (
f'<{meta["successor"]}>; rel="successor-version"'
)
break
return response
Déprécation d’un champ dans la réponse#
Pour déprécier un champ sans le supprimer :
from pydantic import BaseModel, Field
from typing import Optional
class UserV1Response(BaseModel):
id: int
# Champ déprécié : encore présent mais signalé
fullname: Optional[str] = Field(
None,
description="DEPRECATED: Use first_name + last_name instead. Will be removed 2026-01-01.",
json_schema_extra={"deprecated": True}
)
first_name: str
last_name: str
email: str
Changelog#
Un changelog bien tenu est un outil de communication autant qu’un outil de référence. Format recommandé : keepachangelog.com.
## [2.0.0] — 2025-01-15
### BREAKING CHANGES
- GET /users/{id} : champ `fullname` supprimé (déprécié depuis v1.5.0)
- POST /users : champ `phone` désormais obligatoire
### Added
- GET /users/{id}/activity : historique d'activité
- Paramètre `include_deleted=true` sur GET /users
### Deprecated
- GET /v1/reports : sera supprimé le 2025-12-31, utiliser GET /v2/reports
Migration des clients#
La migration d’une version à l’autre n’est pas seulement un problème technique. C’est un problème de coordination et de communication.
Communication proactive#
Avant de déprécier :
Annonce dans le changelog avec date de sunset précise
Email/notification aux équipes consommatrices connues (API key registry)
Headers de déprécation dès le jour de l’annonce
Page de migration dans la documentation avec exemples concrets de changement de code
Période de coexistence#
La durée minimale de coexistence dépend du type d’API :
API interne (équipes internes) : 3 à 6 mois
API partenaires : 6 à 12 mois
API publique : 12 à 24 mois
Ne jamais supprimer sans avoir observé que le trafic sur la version dépréciée est tombé à zéro.
Sunset automatisé#
Un système de sunset automatisé renvoie 410 Gone après la date de sunset et enregistre chaque appel tardif :
from datetime import datetime, timezone
from fastapi import HTTPException
SUNSET_DATES = {
"/api/v1": datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
}
@app.middleware("http")
async def enforce_sunset(request: Request, call_next):
for prefix, sunset_dt in SUNSET_DATES.items():
if request.url.path.startswith(prefix):
if datetime.now(timezone.utc) > sunset_dt:
raise HTTPException(
status_code=410,
detail={
"error": "gone",
"message": f"API v1 has been retired. Use /api/v2.",
"documentation": "https://docs.example.com/migration/v1-to-v2"
}
)
return await call_next(request)
Additive changes — évolution du schéma JSON#
Les changements additifs permettent de faire évoluer une API sans bump MAJOR. Ils nécessitent cependant une discipline de conception.
Optional fields et valeurs par défaut#
# Évolution sans breaking change : ajout de champs optionnels
# v1.0.0 — schéma initial
class ProductV1(BaseModel):
id: int
name: str
price: float
# v1.1.0 — ajout de champs optionnels
class ProductV1_1(BaseModel):
id: int
name: str
price: float
# Nouveaux champs : optionnels avec valeur par défaut
currency: str = "EUR"
tax_rate: Optional[float] = None
tags: list[str] = []
additionalProperties en OpenAPI#
Par défaut, OpenAPI strict interdit les champs supplémentaires (additionalProperties: false). Cette rigueur est contre-productive pour les réponses serveur : elle brise les clients dès qu’on ajoute un champ.
# Dans la spec OpenAPI
components:
schemas:
User:
type: object
required: [id, name]
properties:
id:
type: integer
name:
type: string
# Ne pas mettre additionalProperties: false dans les réponses
# Le laisser à true (défaut) permet l'évolution additive
Règle asymétrique
Appliquez additionalProperties: false dans les schémas de requête (pour rejeter les payloads inattendus). Laissez-le à true (ou absent) dans les schémas de réponse (pour permettre l’évolution sans breaking change).
Évolution des enums#
Les enums dans les réponses sont risqués : ajouter une valeur peut être breaking si les clients ont un switch exhaustif. Solutions :
Documenter explicitement que l’enum peut être étendu
Ajouter une valeur
UNKNOWNdans les enums de réponse pour les clients defensifsUtiliser des chaînes libres avec des constantes documentées plutôt que des enums stricts
API versioning avec OpenAPI#
OpenAPI fournit plusieurs mécanismes pour gérer le versioning dans la spécification.
Le champ info.version#
openapi: "3.1.0"
info:
title: "User API"
version: "2.3.1" # Version SemVer de l'API
description: |
## Changelog
### 2.3.1
- Fix: champ `email` validé correctement
### 2.3.0
- Ajout du endpoint GET /users/{id}/activity
Multiples fichiers de spec#
Pour des versions majeures coexistantes, maintenir un fichier de spec par version est plus lisible qu’un seul fichier multi-version :
api/
openapi_v1.yaml # spec complète de v1
openapi_v2.yaml # spec complète de v2
openapi_v3.yaml # spec en développement
Un script de CI peut vérifier qu’aucun changement dans openapi_v1.yaml n’est breaking (en utilisant des outils comme openapi-diff ou oasdiff).
Redirection des anciens endpoints#
from fastapi.responses import RedirectResponse
@router_v1.get("/users/{user_id}", deprecated=True)
async def get_user_v1_redirect(user_id: int):
"""
**DEPRECATED** — Ce endpoint sera retiré le 2025-12-31.
Utilisez GET /api/v2/users/{user_id}.
"""
return RedirectResponse(
url=f"/api/v2/users/{user_id}",
status_code=301
)
La marque deprecated=True dans FastAPI fait apparaître le endpoint en rayé dans Swagger UI.
Cellules exécutables#
Simulation d’une migration de clients — courbe d’adoption#
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Simulation sur 6 mois (180 jours)
days = np.arange(0, 181)
# Adoption v2 : courbe sigmoïde — lente au début, accélère, plafonne
adoption_v2 = 100 / (1 + np.exp(-0.05 * (days - 90)))
# Trafic v1 : décroissance miroir + bruit
adoption_v1 = 100 - adoption_v2
# Ajouter un bruit réaliste
rng = np.random.default_rng(42)
noise = rng.normal(0, 1.5, len(days))
adoption_v1 = np.clip(adoption_v1 + noise, 0, 100)
adoption_v2 = np.clip(adoption_v2 - noise, 0, 100)
fig, ax = plt.subplots(figsize=(11, 5))
ax.fill_between(days, adoption_v2, alpha=0.2, color="#4c72b0")
ax.fill_between(days, adoption_v1, alpha=0.2, color="#dd8452")
ax.plot(days, adoption_v2, color="#4c72b0", linewidth=2.5, label="API v2 (nouveau)")
ax.plot(days, adoption_v1, color="#dd8452", linewidth=2.5, label="API v1 (déprécié)")
# Événements clés
events = {
0: "Annonce\ndéprécation",
45: "Email\nrelance",
90: "Point\nde bascule",
150: "Sunset\navertissement",
180: "Sunset\ndate"
}
for day, label in events.items():
ax.axvline(x=day, color="gray", linestyle="--", alpha=0.5)
ax.text(day + 1, 105, label, fontsize=8, color="gray", va="bottom")
ax.set_xlabel("Jours depuis l'annonce de déprécation")
ax.set_ylabel("Part du trafic (%)")
ax.set_title("Courbe d'adoption v2 — simulation sur 6 mois")
ax.legend(loc="center right")
ax.set_xlim(0, 180)
ax.set_ylim(0, 115)
plt.show()
Détecteur de breaking changes#
import json
def detect_breaking_changes(schema_v1: dict, schema_v2: dict) -> list[dict]:
"""
Compare deux schémas JSON OpenAPI simplifiés et retourne les breaking changes.
Schéma attendu : {"properties": {...}, "required": [...]}
"""
issues = []
props_v1 = schema_v1.get("properties", {})
props_v2 = schema_v2.get("properties", {})
required_v1 = set(schema_v1.get("required", []))
required_v2 = set(schema_v2.get("required", []))
# Champs supprimés
for field in props_v1:
if field not in props_v2:
issues.append({
"severity": "BREAKING",
"type": "field_removed",
"field": field,
"message": f"Champ '{field}' supprimé — les clients qui le lisent échouent"
})
# Types modifiés
for field in props_v1:
if field in props_v2:
type_v1 = props_v1[field].get("type")
type_v2 = props_v2[field].get("type")
if type_v1 != type_v2:
issues.append({
"severity": "BREAKING",
"type": "type_changed",
"field": field,
"message": f"Type de '{field}' changé : {type_v1} → {type_v2}"
})
# Nouveaux champs obligatoires
new_required = required_v2 - required_v1
for field in new_required:
if field not in props_v1: # nouveau champ obligatoire
issues.append({
"severity": "BREAKING",
"type": "new_required_field",
"field": field,
"message": f"Nouveau champ obligatoire '{field}' — les anciens clients ne l'envoient pas"
})
# Champs ajoutés (non-breaking)
for field in props_v2:
if field not in props_v1:
optional = field not in required_v2
issues.append({
"severity": "SAFE" if optional else "BREAKING",
"type": "field_added",
"field": field,
"message": f"Nouveau champ '{field}' ({'optionnel — OK' if optional else 'obligatoire — BREAKING'})"
})
return issues
# Exemple de comparaison
schema_v1 = {
"properties": {
"id": {"type": "integer"},
"fullname": {"type": "string"},
"email": {"type": "string"},
"age": {"type": "integer"},
},
"required": ["id", "fullname", "email"]
}
schema_v2 = {
"properties": {
"id": {"type": "integer"},
"first_name": {"type": "string"}, # renommé (breaking)
"last_name": {"type": "string"}, # renommé (breaking)
"email": {"type": "string"},
"created_at": {"type": "string"}, # ajout optionnel (safe)
"phone": {"type": "string"}, # ajout obligatoire (breaking)
},
"required": ["id", "first_name", "last_name", "email", "phone"]
}
changes = detect_breaking_changes(schema_v1, schema_v2)
breaking = [c for c in changes if c["severity"] == "BREAKING"]
safe = [c for c in changes if c["severity"] == "SAFE"]
print(f"=== Rapport de breaking changes ===")
print(f"Breaking : {len(breaking)} | Safe : {len(safe)}\n")
for c in changes:
icon = "🔴" if c["severity"] == "BREAKING" else "🟢"
print(f"{icon} [{c['type']}] {c['message']}")
=== Rapport de breaking changes ===
Breaking : 8 | Safe : 1
🔴 [field_removed] Champ 'fullname' supprimé — les clients qui le lisent échouent
🔴 [field_removed] Champ 'age' supprimé — les clients qui le lisent échouent
🔴 [new_required_field] Nouveau champ obligatoire 'last_name' — les anciens clients ne l'envoient pas
🔴 [new_required_field] Nouveau champ obligatoire 'first_name' — les anciens clients ne l'envoient pas
🔴 [new_required_field] Nouveau champ obligatoire 'phone' — les anciens clients ne l'envoient pas
🔴 [field_added] Nouveau champ 'first_name' (obligatoire — BREAKING)
🔴 [field_added] Nouveau champ 'last_name' (obligatoire — BREAKING)
🟢 [field_added] Nouveau champ 'created_at' (optionnel — OK)
🔴 [field_added] Nouveau champ 'phone' (obligatoire — BREAKING)
Cycle de vie d’une API — timeline#
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
fig, ax = plt.subplots(figsize=(13, 4))
phases = [
("Alpha", 0, 3, "#aec7e8"),
("Beta", 3, 6, "#ffbb78"),
("Stable v1", 6, 18, "#98df8a"),
("Deprecated", 18, 24, "#ff9896"),
("Sunset", 24, 25, "#c5b0d5"),
]
y = 0.4
height = 0.4
for label, start, end, color in phases:
rect = mpatches.FancyBboxPatch(
(start, y), end - start, height,
boxstyle="round,pad=0.02",
facecolor=color, edgecolor="white", linewidth=2
)
ax.add_patch(rect)
ax.text(
(start + end) / 2, y + height / 2,
label, ha="center", va="center",
fontsize=10, fontweight="bold", color="#333333"
)
# Événements ponctuels
events = [
(6, "v1.0.0\nGA", "#2ca02c"),
(12, "v1.5.0\nAdditions", "#1f77b4"),
(18, "v2.0.0 GA\n+ v1 deprecated", "#d62728"),
(24, "v1 sunset\n(410 Gone)", "#8c564b"),
]
for x, label, color in events:
ax.axvline(x=x, color=color, linestyle="--", alpha=0.7, linewidth=1.5)
ax.text(x, y + height + 0.08, label, ha="center", va="bottom",
fontsize=8, color=color)
ax.set_xlim(-0.5, 26)
ax.set_ylim(0, 1.2)
ax.set_xlabel("Mois depuis le premier déploiement")
ax.set_title("Cycle de vie d'une API — phases et événements clés")
ax.set_yticks([])
plt.show()
Comparaison des stratégies de versioning#
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import seaborn as sns
sns.set_theme(style="whitegrid", font_scale=0.95)
strategies = ["URI versioning\n(/v2/)", "Header versioning\n(API-Version: 2)", "Content negotiation\n(Accept: vnd.v2+json)"]
criteria = ["Découvrabilité", "Conformité REST", "Facilité\nclient", "Support\noutils", "Caching\nHTTP", "Complexité\nserveur"]
# Scores /5 (subjectif mais documenté)
scores = np.array([
[5, 2, 5, 5, 3, 3], # URI
[2, 5, 4, 3, 4, 3], # Header
[1, 5, 2, 2, 5, 2], # Content negotiation
])
fig, ax = plt.subplots(figsize=(11, 4))
x = np.arange(len(criteria))
width = 0.25
colors = ["#4c72b0", "#dd8452", "#55a868"]
for i, (strategy, score_row, color) in enumerate(zip(strategies, scores, colors)):
bars = ax.bar(x + i * width, score_row, width, label=strategy,
color=color, alpha=0.85, edgecolor="white")
ax.set_xticks(x + width)
ax.set_xticklabels(criteria, fontsize=9)
ax.set_ylabel("Score (/5)")
ax.set_title("Comparaison des stratégies de versioning d'API")
ax.set_ylim(0, 6)
ax.legend(loc="upper right", fontsize=9)
ax.axhline(y=3, color="gray", linestyle=":", alpha=0.5)
plt.show()
Résumé#
Le versioning d’API est un problème de communication autant que de technique. Les points essentiels :
Distinguer systématiquement les changements breaking (suppression, renommage, changement de type) des changements additifs (ajout de champs optionnels, nouveaux endpoints) — seuls les premiers nécessitent un bump MAJOR.
La loi de Postel et le pattern Tolerant Reader rendent les clients résilients aux évolutions additives sans modification de leur code.
Le semantic versioning (MAJOR.MINOR.PATCH) fournit un vocabulaire commun pour communiquer l’impact des changements.
L”URI versioning (
/v2/) reste le choix le plus pragmatique pour les APIs publiques ; le header versioning convient mieux aux APIs internes avec des clients contrôlés.La déprécation suit un protocole : headers
Sunset+Deprecation+Link, changelog, communication proactive, période de coexistence adaptée au type d’API.Dans les specs OpenAPI, laisser
additionalPropertiesàtruedans les schémas de réponse garantit que les ajouts futurs ne cassent pas les clients existants.