---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# 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.

```{admonition} Principe de tolérance
:class: tip
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": 30` en `"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 `200` par `201` peut tromper les clients qui testent l'égalité stricte.

```{admonition} Matrice de risque
:class: note
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 :

```python
# 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
    )
```

```{admonition} Limite du principe
:class: warning
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.

```{admonition} Version 0.x.x
:class: note
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 stabilisation
- `2.0.0-rc.1` : release candidate, corrections uniquement
- `2.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

```{admonition} Recommandation pratique
:class: important
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.
```

```python
# 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

```python
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 :

```python
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](https://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 :

1. **Annonce dans le changelog** avec date de sunset précise
2. **Email/notification** aux équipes consommatrices connues (API key registry)
3. **Headers de déprécation** dès le jour de l'annonce
4. **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 :

```python
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

```python
# É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.

```yaml
# 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
```

```{admonition} Règle asymétrique
:class: tip
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 `UNKNOWN` dans les enums de réponse pour les clients defensifs
- Utiliser 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`

```yaml
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

```python
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

```{code-cell} python3
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

```{code-cell} python3
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']}")
```

### Cycle de vie d'une API — timeline

```{code-cell} python3
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

```{code-cell} python3
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` à `true` dans les schémas de réponse garantit que les ajouts futurs ne cassent pas les clients existants.
