Modèles de requête et de réponse
- Leçon 5
- 40 min
- Module 2 : Créer un petit service FastAPI
Leçon 5 sur 21 · Créer et déployer votre première APIModèles de requête et de réponse
1. Comprendre la requête Web
2. Créer un petit service FastAPI
3. Concevoir un contrat utilisable
4. Documenter l'API avec OpenAPI et Swagger UI
5. Explorer et tester avec Postman
6. Tester et renforcer le service
7. Déployer et vérifier
Ce que vous saurez faire
Ajoutez le point de terminaison qui fait le vrai travail : POST /classify lit un billet de soutien en JSON et renvoie sa catégorie et sa priorité. Les modèles typés vérifient chaque billet avant que votre code le reçoive. Un objet manquant produit donc un code 422 clair, plutôt qu’un plantage.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Les classes de données et les indications de type, présentées dans Les enregistrements structurés : dataclasses : un type pour un billet et Les enregistrements structurés : indications de type.
- Ce que FastAPI fait avec une requête, présenté dans Environnement et premier point de terminaison : ce que fait FastAPI.
- Les codes d’état
200et422, présentés dans Les bases de HTTP : codes d’état.
Ce qu’il vous faut sur votre ordinateur
- Le projet
~/projects/ticket-api, avec son environnement virtuel et FastAPI. Vous l’avez créé dans Environnement et premier point de terminaison : étape 1 : créer le projet. ticket_api/main.pyavecGET /health, présenté dans Environnement et premier point de terminaison : étape 2 : le premier point de terminaison. Cette leçon remplacemain.py. Un point de terminaison supplémentaire comme/versionn’a donc pas d’importance.- Le serveur de développement fonctionne dans le dossier du projet, avec l’environnement activé. Démarrez-le comme dans Environnement et premier point de terminaison : étape 3 : démarrer le serveur. Vérifiez-le comme dans Étape 4 : vérifier le fonctionnement.
- Si le projet manque ou ne démarre pas, refaites les étapes 1 à 4 d’Environnement et premier point de terminaison.
- Un deuxième terminal dans le dossier du projet, pour curl. Sous Windows, tapez
curl.exe.
Testé avec Python 3.14.6, FastAPI 0.142.2 et Pydantic 2.13 sur macOS.
Les modèles décrivent les données
Section intitulée « Les modèles décrivent les données »Dans le cours Python, find_problems vérifiait chaque billet à la main : l’objet est-il présent? La priorité est-elle un nombre de 1 à 3? Une API reçoit du JSON de programmes que vous ne contrôlez pas. Elle doit donc faire les mêmes vérifications, pour chaque champ de chaque requête.
FastAPI utilise Pydantic pour cela. Un modèle Pydantic est une classe qui énumère les champs de données, avec un type et des limites pour chacun. Il ressemble à une classe de données :
from pydantic import BaseModel, Field
class TicketIn(BaseModel): subject: str = Field(min_length=1, max_length=120) body: str = Field(default="", max_length=5000)| Partie | Signification |
|---|---|
class TicketIn(BaseModel) |
Un modèle pour l’entrée du point de terminaison : un billet. |
subject: str |
Le champ doit être une chaîne de caractères. Il n’a aucune valeur par défaut, donc il est obligatoire. |
Field(min_length=1, max_length=120) |
Limites : au moins 1 caractère, au plus 120. |
body: str = Field(default="", ...) |
Facultatif. Si le client l’omet, sa valeur est "". |
Une fonction de point de terminaison peut avoir un paramètre dont le type est un modèle. FastAPI lit alors le corps JSON et le vérifie selon le modèle, puis appelle votre fonction. Si le corps ne respecte pas le modèle, FastAPI répond 422 et votre fonction ne s’exécute jamais.
Le modèle de réponse
Section intitulée « Le modèle de réponse »La sortie a aussi un modèle. FastAPI lit le type de retour de la fonction. Il l’utilise pour vérifier le résultat et écrire le JSON :
from typing import Literal
Category = Literal["billing", "login", "shipping", "account", "other"]
class Classification(BaseModel): category: Category priority: int = Field(ge=1, le=3) confidence: float = Field(ge=0, le=1) model_version: strLiteral[...] signifie « une seule de ces valeurs ». ge et le signifient « supérieur ou égal à » et « inférieur ou égal à ». Le modèle de réponse est une promesse à chaque client : la catégorie est l’un des cinq mots possibles. La priorité est toujours 1, 2 ou 3. Si votre code tente de rompre cette promesse, Pydantic l’arrête (voir l’erreur courante ci-dessous).
Exemple détaillé : le point de terminaison de classification
Section intitulée « Exemple détaillé : le point de terminaison de classification »Pour l’instant, la classification est simple : la première catégorie dont le mot-clé apparaît dans le texte l’emporte. La prochaine leçon la déplace dans son propre module et l’améliore. Voici le fichier main.py complet :
from typing import Literal
from fastapi import FastAPIfrom pydantic import BaseModel, Field
app = FastAPI(title="Ticket Classifier API")
Category = Literal["billing", "login", "shipping", "account", "other"]
KEYWORDS = { "billing": ("invoice", "charged", "refund", "payment"), "login": ("password", "sign in", "log in", "two-factor", "locked out"), "shipping": ("parcel", "delivery", "courier", "tracking"), "account": ("account", "company name", "user"),}
class TicketIn(BaseModel): subject: str = Field(min_length=1, max_length=120) body: str = Field(default="", max_length=5000)
class Classification(BaseModel): category: Category priority: int = Field(ge=1, le=3) confidence: float = Field(ge=0, le=1) model_version: str
@app.get("/health")def health() -> dict[str, str]: return {"status": "ok"}
@app.post("/classify")def classify(ticket: TicketIn) -> Classification: text = f"{ticket.subject}\n{ticket.body}".lower() for category, words in KEYWORDS.items(): if any(word in text for word in words): return Classification( category=category, priority=2, confidence=0.7, model_version="keywords-0.1" ) return Classification( category="other", priority=3, confidence=0.2, model_version="keywords-0.1" )Dans la fonction, ticket est un objet TicketIn, déjà vérifié : ticket.subject est une chaîne de 1 à 120 caractères. La fonction ne fait aucune vérification elle-même. Elle renvoie une Classification, et FastAPI la transforme en JSON.
Requêtes valides et invalides
Section intitulée « Requêtes valides et invalides »Amira conserve ses corps de requête dans des fichiers, dans un dossier requests. Elle peut ainsi les envoyer de nouveau facilement. Voici un billet valide :
{"subject": "Charged twice for March", "body": "My card was charged two times for the same invoice."}curl -i -X POST http://127.0.0.1:8000/classify -H "Content-Type: application/json" --data @requests/billing.jsonHTTP/1.1 200 OK{"category":"billing","priority":2,"confidence":0.7,"model_version":"keywords-0.1"}Puis trois billets invalides. Elle prédit le code d’état avant chaque envoi :
| Fichier | Corps | Réponse |
|---|---|---|
empty-subject.json |
{"subject": ""} |
422 : "type":"string_too_short", "loc":["body","subject"], "msg":"String should have at least 1 character" (la chaîne doit contenir au moins 1 caractère) |
no-subject.json |
{"body": "Hello"} |
422 : "type":"missing", "loc":["body","subject"], "msg":"Field required" (champ obligatoire) |
wrong-types.json |
{"subject": 42, "body": ["a"]} |
422, avec deux erreurs : subject et body ne sont pas des chaînes de caractères |
Voici le corps complet pour l’objet vide :
{"detail":[{"type":"string_too_short","loc":["body","subject"],"msg":"String should have at least 1 character","input":"","ctx":{"min_length":1}}]}loc indique l’emplacement du problème : dans le body (corps), le champ subject (objet). FastAPI signale tous les problèmes à la fois, comme find_problems dans le cours Python. Dans le module 3, vous remplacerez ce format par un format plus court, identique pour toutes les erreurs.
Exercice guidé : créer et appeler le point de terminaison
Section intitulée « Exercice guidé : créer et appeler le point de terminaison »-
Remplacez
ticket_api/main.pypar l’exemple détaillé et enregistrez-le. Le serveur de développement redémarre. -
Créez le dossier
requestsavec les quatre fichiers du tableau :billing.json,empty-subject.json,no-subject.jsonetwrong-types.json. -
Envoyez chaque fichier avec curl, comme dans l’exemple détaillé. Avant chaque envoi, notez le code d’état attendu.
billing.jsondonne200. Les trois autres donnent422, avec les valeurs delocet demsg(message) du tableau. -
Envoyez un corps avec un champ supplémentaire :
{"subject": "Refund", "colour": "red"}.Il donne
200. Pydantic ignore les champs absents du modèle. Le module 3 fera en sorte que l’API les rejette. Une faute de frappe dans un nom de champ ne sera donc plus ignorée silencieusement. -
Envoyez un objet de 121 caractères. (Créez le fichier avec Python :
python -c "import json; print(json.dumps({'subject': 'x' * 121}))" > requests/long.json.)Il donne
422avecstring_too_long(chaîne trop longue). Essayez ensuite exactement 120 caractères :200. Vous avez testé les deux côtés de la limite.
À vous de jouer : un nouveau champ facultatif
Section intitulée « À vous de jouer : un nouveau champ facultatif »Le service de soutien envoie le canal de chaque billet : email (courriel), phone (téléphone) ou chat (clavardage). Ajoutez à TicketIn un champ facultatif channel, avec la valeur par défaut "email", qui accepte uniquement ces trois valeurs. Ajoutez ensuite le canal à la réponse, sous forme de champ channel dans Classification. Le client voit ainsi quel canal l’API a utilisé.
Testez quatre corps : avec "channel": "chat", sans canal, avec "channel": "fax" et avec "channel": "Chat". Prédisez d’abord chaque résultat.
Afficher une réponse
Channel = Literal["email", "phone", "chat"]
class TicketIn(BaseModel): subject: str = Field(min_length=1, max_length=120) body: str = Field(default="", max_length=5000) channel: Channel = "email"
class Classification(BaseModel): category: Category priority: int = Field(ge=1, le=3) confidence: float = Field(ge=0, le=1) model_version: str channel: ChannelDans classify, passez channel=ticket.channel aux deux appels de Classification(...). "chat" donne 200 avec "channel":"chat". Sans canal, vous obtenez "channel":"email". "fax" et "Chat" donnent tous deux 422 avec literal_error : un Literal compare les valeurs exactement, donc les majuscules comptent. Retirez de nouveau le champ avant la prochaine leçon. Le reste du cours ne l’utilise pas.
Erreur courante
Erreur 500 Internal Server Error causée par votre propre résultat
Symptôme : une requête qui semble valide reçoit 500 Internal Server Error (erreur interne du serveur). Son corps est Internal Server Error, et non du JSON. Le terminal du serveur affiche une longue trace d’appels qui se termine ainsi :
pydantic_core._pydantic_core.ValidationError: 1 validation error for Classificationpriority Input should be greater than or equal to 1 [type=greater_than_equal, input_value=0, input_type=int]Cause : votre code a créé une Classification qui ne respecte pas les limites du modèle, ici priority=0. Pydantic vérifie les valeurs lorsque vous créez le modèle et les refuse. C’est un défaut du code, pas un problème de requête, donc le client reçoit 500. Le modèle a fait son travail : une priorité incorrecte n’a jamais atteint un client.
Correction :
- Lisez les dernières lignes de la trace d’appels : le modèle, le champ, la valeur et la règle.
- Trouvez où votre code produit cette valeur et corrigez-le.
- N’assouplissez pas le modèle pour faire disparaître l’erreur. La limite fait partie de la promesse à vos clients.
Vérifiez votre compréhension
Un client envoie {« subject » : « »}. La fonction classify s'exécute-t-elle? Que reçoit le client?
Afficher la réponse
La fonction ne s’exécute pas. FastAPI vérifie d’abord le corps selon TicketIn et constate que subject contient moins de 1 caractère. Il répond 422 Unprocessable Content (contenu impossible à traiter) avec une erreur dont loc vaut ["body", "subject"]. Son type est string_too_short (chaîne trop courte). La fonction ne reçoit que des billets valides.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Pourquoi le point de terminaison renvoie-t-il un modèle Classification plutôt qu'un simple dictionnaire?
Afficher la réponse
Le modèle est une promesse vérifiée. Pydantic refuse un résultat qui la rompt, comme une catégorie inconnue ou une priorité de 0. Le client ne reçoit donc jamais de valeur incorrecte. FastAPI utilise aussi le modèle pour documenter la réponse. Avec un dictionnaire, n’importe quelle valeur pourrait atteindre le client.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Un modèle Pydantic énumère les champs des données, avec leurs types, leurs limites et leurs valeurs par défaut.
- Un paramètre typé avec un modèle fait lire et vérifier le corps JSON par FastAPI avant l’exécution de votre fonction. Une entrée invalide reçoit
422avec tous les problèmes. - Le type de retour est le modèle de réponse : une promesse vérifiée sur la sortie.
- Conservez les corps de requête de test dans des fichiers et testez les deux côtés de chaque limite.
Ensuite, vous déplacerez le classificateur hors de main.py et donnerez à chaque partie de l’API son propre module.
Leçon suivante : Séparer les responsabilités