backend / fastapi
Réponses et status codes
Explication
Ce que vous allez apprendre
- Comprendre le rôle exact de
response_modelcomme filtre de sortie - Choisir le bon code de statut HTTP selon la sémantique de l'action (201, 204, 404...)
- Contrôler la sérialisation des champs
Noneavecresponse_model_exclude_none - Renvoyer des réponses personnalisées (redirection, headers custom, fichier)
- Documenter plusieurs réponses possibles pour un même endpoint dans Swagger
Dans quel contexte ?
L'endpoint GET /products/{id} de app/routers/products.py retourne un objet interne qui contient un champ cost_price (le prix d'achat, confidentiel) en plus du prix de vente public. Sans response_model, ce champ sensible finirait exposé tel quel dans la réponse JSON à n'importe quel client de l'API. Déclarer response_model=ProductOut, un modèle Pydantic qui ne contient volontairement pas cost_price, garantit que ce champ ne sera jamais sérialisé, quelle que soit l'évolution future du code interne.
Une idée souvent mal comprise au début
La valeur que retourne ta fonction Python n'est PAS forcément ce qui part sur le réseau. Un objet SQLAlchemy ou un dictionnaire riche peut contenir bien plus de champs que ce que le client verra.
Le response_model agit comme un filtre entre les deux. Il définit un contrat de sortie explicite, indépendant de la structure interne de tes données.
Concrètement, même si l'objet retourné a vingt champs, seuls ceux déclarés dans le response_model sont sérialisés en JSON. Les autres restent invisibles pour le client, sans code supplémentaire à écrire.
Cette séparation a un double intérêt à connaître. D'abord la sécurité : ne jamais exposer un champ sensible par accident.
Ensuite la stabilité : la structure de ta base de données peut évoluer sans casser le contrat que voient tes clients API. Tant que le response_model reste stable, rien ne change pour eux.
Une fois cette séparation posée, il reste à bien choisir le code de statut renvoyé. Ce n'est pas un détail cosmétique, mais un langage standardisé que comprennent tous les clients HTTP.
201 signifie "création réussie", 204 "succès, mais rien à renvoyer", 404 "introuvable". Respecter cette sémantique permet à n'importe quel client de réagir correctement sans lire ta documentation en détail.
| Code | Signification | Cas d'usage typique |
|---|---|---|
| 200 | Succès avec contenu | GET réussi |
| 201 | Création réussie | POST qui crée une ressource |
| 204 | Succès, aucun contenu | DELETE réussi |
| 404 | Ressource introuvable | GET/PUT/DELETE sur un id inexistant |
Piège fréquent
Un code 204 ne doit, selon la spécification HTTP, jamais contenir de corps de réponse, c'est pour cela que la fonction retourne None dans ce cas. De même, response_model_exclude_none évite de polluer une réponse de champs null, mais peut aussi masquer un champ que le client attendait explicitement à null plutôt qu'absent.
Commandes & code
Réponses et status codes
from fastapi import FastAPI, status
from pydantic import BaseModel
app = FastAPI()
class ProductOut(BaseModel):
id: int
name: str
price: float
# response_model filtre/valide la sortie, INDÉPENDAMMENT de ce que retourne la fonction
@app.get("/products/{id}", response_model=ProductOut)
def get_product(id: int):
# même si l'objet interne a des champs en plus (ex. "cost_price"), seuls ceux
# du response_model sont sérialisés dans la réponse JSON
return get_product_from_db(id)
def get_product_from_db(id: int):
class InternalProduct:
id = 1
name = "Clavier"
price = 49.99
cost_price = 22.0 # jamais exposé grâce au response_model
return InternalProduct()# Status codes explicites selon la sémantique HTTP
@app.post("/products", response_model=ProductOut, status_code=status.HTTP_201_CREATED)
def create_product(product: ProductOut):
return product
@app.delete("/products/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_product(id: int):
delete_from_db(id)
return None # 204 : pas de contenu dans la réponse
def delete_from_db(id: int):
pass# response_model_exclude_none / exclude_unset — contrôle fin de la sérialisation
class UserOut(BaseModel):
id: int
username: str
bio: str | None = None
avatar_url: str | None = None
@app.get("/users/{id}", response_model=UserOut, response_model_exclude_none=True)
def get_user(id: int):
# les champs None (bio, avatar_url) sont omis de la réponse JSON plutôt que "null"
return UserOut(id=id, username="alice", bio=None, avatar_url=None)# Réponses personnalisées — JSONResponse, headers custom, redirection
from fastapi.responses import JSONResponse, RedirectResponse, FileResponse
@app.get("/legacy-endpoint")
def legacy_redirect():
return RedirectResponse(url="/new-endpoint", status_code=status.HTTP_308_PERMANENT_REDIRECT)
@app.get("/custom-headers")
def with_custom_headers():
return JSONResponse(
content={"message": "ok"},
headers={"X-Custom-Header": "valeur", "Cache-Control": "no-store"},
)
@app.get("/download/{filename}")
def download_file(filename: str):
return FileResponse(f"./files/{filename}", media_type="application/octet-stream")# Documenter plusieurs réponses possibles pour Swagger UI
@app.get(
"/products/{id}",
responses={
404: {"description": "Produit introuvable"},
200: {"description": "Produit trouvé", "model": ProductOut},
},
)
def get_product_documented(id: int):
return get_product_from_db(id)Résumé
response_modelfiltre la sortie selon un schéma explicite, indépendamment de l'objet retourné par la fonction.status_codedocumente et fixe le code HTTP de succès (201 pour une création, 204 pour une suppression sans contenu).response_model_exclude_noneévite de polluer les réponses JSON avec des champsnull.- Le paramètre
responses={}enrichit la documentation OpenAPI avec les cas d'erreur possibles.
Exercices pratiques
Mission : le prix d'achat qui fuite dans l'API publique
Objectif : Corriger un endpoint GET /products/{id} qui expose un champ confidentiel et un endpoint DELETE qui renvoie un mauvais code de statut.
Contexte
Sur app/routers/products.py, l'endpoint GET /products/{id} retourne un objet interne complet, y compris cost_price (le prix d'achat confidentiel). Par ailleurs, DELETE /products/{id} renvoie actuellement un code 200 avec un corps JSON vide, ce qui perturbe un client strict sur la sémantique HTTP.