
Introduction
Imaginez.
Vous êtes une dev assez expérimentée et vous fabriquez un logiciel destiné aux bibliothèques municipales.
Par ailleurs, vous encadrez d’autres personnes plus juniors.
L’une d’entre elles (appelons-là Camille) a eu pour tâche d’exposer une API au-dessus de votre base de données qui permet de gérer les fonds.
C’est l’heure de la revue de code, et Camille vous montre une API qui ressemble à ceci :
GET /api/book/<isbn>
# Get book details using its ISBM
POST /api/check-out/book/<isbn>
# Check out a book from the library
Comme vous avez déjà été confrontée au problème des mises à jour d’API, vous dites à Camille de rajouter un numéro de version au début de chaque route, comme ceci:
GET /api/v1/book/<isbn>
POST /api/v1/check-out/book/<isbn>
"Ainsi", expliquez-vous à Camille, "le jour où on change d'avis sur l'API, on pourra faire une v2 sans casser l'ancien code".
"Oh je comprends", répond Camille.
Quelques temps plus tard, après s'être assuré que la nouvelle API est correctement documentée, vous la déployez et la présentez aux autres équipes.
L'API a beaucoup de succès, et quelques mois plus tard c'est au moins 3 équipes différentes qui utilisent votre API régulièrement : bien joué !
La tentation de la v2
Le temps a passé, et Camille revient vous voir :
"Dis, j'ai réfléchi et j'ai eu des idées pour améliorer l'API. Par exemple, la route api/book/<isbn> n'est pas très cohérente. On documente qu'il faut passer l'ISBN directement dans le chemin de la route, mais on renvoie un JSON avec ce schéma":
{
"book": {
"id": "9791020904409"
"author": "Pablo Servigne",
"title": "L'Entraide, l'autre loi de la jungle"
}
}
ça cause pas mal de confusion, et puis c'est déjà arrivé que les ISBN changent de format
Du coup, vu qu'on a bien pris soin de faire une v1, est-ce que ça ne serait pas l'occasion de faire une V2 ?
On aurait juste GET /api/v2/book et on pourrait passer isbn dans les paramètres:
Et le JSON aurait cette forme là:
GET /book?isbn=2370735767
{
"book": {
"isbn": "2370735767",
"author": "Mehdi Moussaîd",
"title:" A-t-on besoin d'un chef ?"
}
}
Qu'est-ce que tu en penses?"
Spontanément, vous avez envie de dire oui : après tout, l'API V2 a l'air bien plus propre.
Mais d'un autre côté, vous hésitez. L'API V1 est déjà en production. Certes, il y a bien eu quelques bugs liés à la gestion des IDs, mais ils ont tous été corrigés depuis.
Pourquoi ne PAS faire une v2
Vous décidez de ne pas prendre de décision tout de suite, et à la place vous vous posez avec Camille pour une séance de brainstorming.
A la sortie de la réunion, vous vous êtes mises d'accord sur le fait que le scénario le plus probable est le suivant:
1. La v2 est déployée en parallèle de la v1 (de façon à ne pas casser la prod)
2. Les autres équipes savent qu'il faudra faire le passage de v1 à v2, mais il n'y a pas de caractère d'urgence.
3. Chaque équipe se retrouve donc avec un ticket "mettre à jour vers la v2" dans son backlog
4. Ces tickets considérés (à raison) par le métier comme étant purement techniques donc ils ne sont jamais priorisés
5. Quelques équipes (mais pas toutes) mettent à jour parce que la v2 leur apporte de vrais avantages - ou bien elles commencent à utiliser l’API directement en version 2
6. Il y a un bug en production facile à corriger en v2 mais pas en v1 et comme vous ne pouvez pas forcer les équipes à mettre à jour, vous devez faire deux correctifs différents, les tester et les déployer sans rien casser.
Bref. Beaucoup d’ennuis en perspective …
Que faire à la place ?
Et bien, ne jamais faire de 2.0, tout simplement :)
L'idée est de considérer qu'une fois que la 1.0 est sortie et publique, c'est votre travail de ne jamais casser la rétrocompatibilité
Dans le cas qui nous occupe, vous pouvez tout à fait garder la route "legacy" GET /api/v1/book/<isbn> en plus de la route qui prend l'id en paramètre.
Et de la même façon, vous pouvez renvoyer un JSON qui contient les deux champs:
{
"book": {
"id": "2370735767",
"isbn": "2370735767",
"author": "Mehdi Moussaîd",
"title:" A-t-on besoin d'un chef ?"
}
}
Si on imagine que le backend est en Python/FASTAI que les clients sont en TypeScript, cela donnerait:
class BookDetails(BaseModel):
id: str
isbn: str
class BookResponse(BaseModel):
book: BookResponse
@app.get("/book/{id}")
def get_book_legacy(id: Annotated[str, Path(title="book id")]):
""" Deprecated end point, use /book and pass the ISBN as query string instead """
return get_book_by(id)
class GetBookQuery(BaseModel):
isbn: str
@app.get("/book")
def get_book_by_id(query: Annotated[GetBookQuery, Query()]) -> GetBookResponse:
return get_book_by_id(id=query.isbn)
def get_book_by_id(id: str) -> BookResponse:
"""
Get a book, using its id
"""
book = repo.get_book_by_id(id)
book_details = BookDetails(
id=book.isbn, # for retro-compatibility
isbn=book.isbn
)
return BookResponse(
book=book_details
)
Pour un client qui ne s'est jamais mis à jour:
interface Book = {
id: string,
title: string,
autor: string
}
async function getBookById(id: string) : Book {
const reponse = client.get(`/book/{id}`);
return response as Book
}
Et pour un client qui a lu la doc et est passé aux API recommandées :
et n'utilise que l'ISBN:
interface Book = {
isbn: string,
title: string,
autor: string
}
async function getBookById(isbn: string) : Book {
const response = client.get(`/book/`, { params: { isbn }});
return response as Book
}
Notez que l’API est documentée comme dépréciée, mais elle ne sera jamais vraiment supprimée
Comment tester?
Vous pouvez utiliser des tests de contrat :
def test_get_book_id_in_path(test_client, fake_repo):
fake_repo.add_book(isbn='123456789012', author="Jerry Z. Muller", title="The Tyranny of Metrics")
json = client.get("/book/123456789012")
assert json["title"] == "The Tyranny of Metrics"
assert json["author"] == "Jerry Z. Muller"
assert json["id"] == "123456789012"
def test_get_book_usigg_isnb_in_params(client, fake_repo)
fake_repo.add_book(id='abc123', author="Jerry Z. Muller", title="The Tyranny of Metrics")
json = client.get("/book", params={"isbn": "123456789012"})
assert json["title"] == "The Tyranny of Metrics"
assert json["author"] == "Jerry Z. Muller"
assert json["isbn"] == "123456789012"
Notez qu'on utilise surtout pas les objets BaseModel déclarés au niveau du back-end, le but c'est justement que ces tests cassent si jamais l'API devient incompatible !
Conclusion
J’espère que vous avez apprécié cet exemple. Il n’est pas si fictif que cela, on peut penser notamment à Python, dont le passage de 2 à 3 a causé a duré près de 10 ans (!) et a causé pas mal de souci à la communauté.
Et on peut aussi citer Rust et Go, qui ont sorti leur version 1.0 en 2012 et 2015 respectivement et n’ont jamais cassé leur rétrocompatibilité.
En tout cas, j’espère que vous cela vous aura donné à réfléchir, et que vous vous poserez davantage de questions avant de proposer une mise à jour incompatible aux personnes qui utilisent vos APIs 🙂


.png)

.png)

