Aller au contenu
English

Conception des ressources et des points de terminaison

  • Leçon 7
  • 35 min
  • Module 3 : Concevoir un contrat utilisable

Ce que vous saurez faire

Donnez à l’API des routes qu’une personne qui ne la connaît pas peut deviner. Donnez-lui aussi une version pour pouvoir la modifier sans briser les programmes qui l’utilisent déjà. Ensuite, écrivez le contrat : chaque requête et la réponse exacte en cas de réussite ou de chaque échec.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 35 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Testé avec Python 3.14.6 et FastAPI 0.142.2 sur macOS.

Quand un autre programme utilise votre API, son code dépend de vos chemins, noms de champs et codes de statut. Si vous renommez category en team, ce programme ne fonctionne plus. Vous ignorez peut-être même son existence. Ensemble, les chemins, champs et codes de statut forment le contrat de l’API. C’est une promesse à chaque client (un programme qui appelle l’API).

Concevez donc le contrat de façon réfléchie, avant que de nombreux clients en dépendent. Cette leçon présente quatre règles.

Une ressource est une chose que l’API offre : des billets de soutien, des catégories, une classification. Un chemin REST nomme la ressource avec un nom, et la méthode indique quoi en faire :

Au lieu de Écrivez Pourquoi
GET /getCategories GET /v1/categories La méthode dit déjà « obtenir ».
POST /tickets/delete/T-1 DELETE /v1/tickets/T-1 La méthode indique l’action.
GET /ticket/T-1 GET /v1/tickets/T-1 Les collections sont au pluriel : /tickets désigne « tous » et /tickets/T-1 désigne « un seul ».

Un point de terminaison de cette API, /classify, est un verbe. C’est une exception acceptée : la classification est une action qui ne crée rien. Un nom comme /classifications laisserait croire que l’API les conserve. Choisissez le nom le plus clair, puis gardez-le.

Choisissez un style une fois, puis utilisez-le dans chaque chemin et chaque champ :

  • Chemins : lettres minuscules, noms au pluriel, - entre les mots : /v1/ticket-categories.
  • Champs JSON : lettres minuscules avec _ entre les mots : model_version. C’est le style Python, donc les modèles n’ont besoin d’aucune traduction.
  • Valeurs qu’un programme compare : mots fixes en minuscules : "billing", pas "Billing".

Une personne qui développe un client peut alors deviner les autres points de terminaison après en avoir vu un.

Certains changements brisent les clients. D’autres non :

Incompatible : un client peut échouer Compatible : les anciens clients continuent de fonctionner
Renommer ou supprimer un champ, un chemin ou une valeur Ajouter un point de terminaison
Rendre obligatoire un champ de requête facultatif Ajouter un champ de requête facultatif
Changer le type ou le sens d’un champ Ajouter un champ à une réponse
Renvoyer un code de statut différent pour le même cas Accepter plus de valeurs qu’avant

Pour un changement incompatible, publiez une nouvelle version de l’API. Gardez l’ancienne en service jusqu’à ce que ses clients passent à la nouvelle. Le plus simple est d’ajouter une version au début du chemin : /v1/classify maintenant, /v2/classify plus tard. Dans chaque URL, les clients voient quelle version ils utilisent.

/health reste sans version. Il concerne le service en cours d’exécution, pas le contrat. Les outils d’hébergement s’attendent à le trouver à un endroit fixe.

Un tableau de contrat énumère les requêtes importantes de chaque point de terminaison et la réponse exacte pour chacune. Il sert de plan pour le code, la documentation et les tests. Voici le tableau du point de terminaison de classification, avec le format d’erreur que vous créerez dans la prochaine leçon :

Requête Statut Corps de la réponse
POST /v1/classify {"subject": "Charged twice for March"} 200 {"category": "billing", "priority": 1, "confidence": 0.7, "model_version": "keywords-1.0"}
{"subject": "Hello"} (aucun mot-clé) 200 {"category": "other", "priority": 3, ...}
{"subject": ""} 422 {"error": {"code": "invalid_request", "fields": ["subject"], ...}}
Aucun champ subject 422 Même structure, "fields": ["subject"]
Un champ absent du modèle, comme "colour" 422 Même structure, "fields": ["colour"]
Corps qui n’est pas du JSON valide 422 Même structure, "fields": ["body"]
GET /v1/classify 405 {"error": {"code": "method_not_allowed", ...}}

Écrire d’abord les lignes d’échec est une bonne habitude. La plupart des bogues et des questions des clients les concernent.

Exemple détaillé : un préfixe de version et une deuxième ressource

Section intitulée « Exemple détaillé : un préfixe de version et une deuxième ressource »

Amira ajoute la version à chaque route de routes.py avec un seul changement : le routeur reçoit un préfixe. Elle ajoute aussi une deuxième ressource, la liste des catégories. Un client peut ainsi les afficher dans un menu sans les copier de la documentation :

ticket_api/routes.py (changes)
from typing import Annotated, get_args
from ticket_api.models import Category, CategoryList, Classification, TicketIn
router = APIRouter(prefix="/v1", tags=["tickets"])
@router.get("/categories")
def list_categories() -> CategoryList:
return CategoryList(categories=list(get_args(Category)))
ticket_api/models.py (add at the end)
class CategoryList(BaseModel):
categories: list[Category]

prefix="/v1" place /v1 devant chaque chemin de ce routeur : /classify devient /v1/classify. tags regroupe les routes dans la documentation (module 4). get_args(Category) donne les valeurs du type Literal. La liste correspond donc toujours au modèle de réponse : il n’y a qu’un endroit à modifier.

La réponse est un objet avec un champ, {"categories": [...]}, et non une simple liste. On pourra ainsi ajouter un champ, comme une description pour chaque catégorie, sans changement incompatible.

Terminal (on Windows: curl.exe)
curl http://127.0.0.1:8000/v1/categories
Sortie
{"categories":["billing","login","shipping","account","other"]}

L’ancien chemin n’existe plus :

Terminal (on Windows: curl.exe)
curl -X POST http://127.0.0.1:8000/classify -H "Content-Type: application/json" --data @requests/billing.json
Sortie
{"detail":"Not Found"}
  1. Modifiez routes.py et models.py comme dans l’exemple détaillé. Enregistrez.
  2. Appelez GET /v1/categories. Vous voyez les cinq catégories.
  3. Envoyez requests/billing.json à /v1/classify. Le résultat est le même qu’avant.
  4. Envoyez-le à l’ancien chemin /classify. Vous obtenez 404.
  5. Appelez GET /health. Il fonctionne toujours, sans /v1.

À vous de jouer : le tableau de contrat des catégories

Section intitulée « À vous de jouer : le tableau de contrat des catégories »

Écrivez le tableau de contrat de GET /v1/categories, sous la même forme que le tableau ci-dessus. Incluez au moins une ligne d’échec. Puis vérifiez chaque ligne avec curl.

Afficher une réponse
Requête Statut Corps de la réponse
GET /v1/categories 200 {"categories": ["billing", "login", "shipping", "account", "other"]}
GET /v1/categories?lang=fr (un paramètre de requête inconnu) 200 Le même. Les paramètres de requête inconnus sont ignorés.
POST /v1/categories 405 {"error": {"code": "method_not_allowed", ...}} après la prochaine leçon. Maintenant : {"detail":"Method Not Allowed"}
GET /v1/category (singulier) 404 {"error": {"code": "not_found", ...}} après la prochaine leçon. Maintenant : {"detail":"Not Found"}

Le tableau révèle un écart : actuellement, la structure des corps d’erreur diffère de celle que promet le contrat. La prochaine leçon corrige cet écart.

Erreur courante

Un petit changement de nom qui brise un client

Symptôme : après un « petit nettoyage », une équipe cliente signale que son programme affiche une erreur ou des valeurs vides. Pourtant, votre API « fonctionne » : chaque requête reçoit 200.

Cause : le nettoyage a renommé un champ, par exemple model_version en version, ou une valeur, par exemple "login" en "sign-in". La réponse est valide pour votre nouveau modèle, mais le client lit encore l’ancien nom et ne reçoit rien. Renommer est un changement incompatible, même quand rien n’échoue de votre côté.

Solution :

  1. Rétablissez l’ancien nom maintenant pour que les clients fonctionnent à nouveau.
  2. Si le nouveau nom est vraiment nécessaire, ajoutez-le à côté de l’ancien et informez les clients. Supprimez l’ancien seulement dans la prochaine version (/v2).
  3. Au module 4, vous ajouterez un test qui échoue quand le contrat change. Cette erreur ne pourra donc pas atteindre les clients sans être détectée.

Vérifiez votre compréhension

Quels changements à /v1/classify sont incompatibles? (a) Ajouter un champ de requête facultatif lang. (b) Rendre body obligatoire. (c) Ajouter un champ de réponse explanation. (d) Renvoyer 400 au lieu de 422 pour un subject vide.

Afficher la réponse

(b) et (d) sont incompatibles. (b) : les clients qui n’envoient pas body reçoivent maintenant une erreur. (d) : les clients qui vérifient 422 ne détectent plus l’erreur. (a) et (c) sont compatibles : les anciens clients n’envoient pas lang et ne lisent pas explanation. Ils continuent donc de fonctionner. Les changements incompatibles vont dans /v2.

Récapitulatif

  • Les chemins, champs et codes de statut forment un contrat avec chaque client. Concevez-le de façon réfléchie.
  • Les chemins nomment les ressources avec des noms au pluriel. La méthode indique quoi faire.
  • Gardez un seul style de nommage pour les chemins, les champs et les valeurs.
  • Placez une version dans le chemin (/v1). Un changement incompatible reçoit une nouvelle version. Un ajout n’en reçoit pas.
  • Écrivez un tableau de contrat en commençant par les lignes d’échec.

Ensuite, vous donnerez la même structure à chaque erreur. Vous veillerez aussi à ce qu’aucune erreur ne révèle le fonctionnement interne du serveur.

Leçon suivante : Validation et réponses d'erreur