Routing et validation FastAPI : le guide pratique
Le routing et la validation, c'est l'essentiel de ce qui rend un service FastAPI propre. Voici comment APIRouter, les contraintes Pydantic, les response models et les erreurs custom s'articulent, et ce qui a changé en 2026.
Le routing et la validation sont les deux choses que FastAPI fait pour vous à chaque requête : il choisit la fonction qui traite l'appel, et il vérifie que les données entrantes correspondent à ce que cette fonction attend. Bien faire les deux, c'est l'essentiel du travail d'une API propre. Ce guide parcourt leur fonctionnement, les patterns qui tiennent sur un gros codebase, et les changements arrivés en 2026.
Comment fonctionne le routing : APIRouter et include_router
Un APIRouter est un groupe d'endpoints montable, sans serveur propre. Vous construisez un router par ressource ou par domaine, chacun dans son module, puis vous les assemblez dans votre point d'entrée. Un router porte un préfixe commun, un jeu de tags OpenAPI, et des dépendances optionnelles qui s'exécutent pour chacune de ses routes.
# routers/orders.py
from fastapi import APIRouter
router = APIRouter(prefix="/orders", tags=["orders"])
@router.get("")
async def list_orders():
...
@router.get("/{order_id}")
async def get_order(order_id: int):
...
# main.py
from fastapi import FastAPI
from routers import orders, users
app = FastAPI()
app.include_router(orders.router)
app.include_router(users.router)Routers imbriqués et dépendances partagées
Un router peut en inclure un autre : vous composez un arbre, par exemple un router admin qui exige un administrateur authentifié et qui contient des sous-routers pour chaque zone d'administration. Une dépendance déclarée au niveau du router s'exécute avant chaque route de ce router, ce qui est le bon endroit pour l'authentification, la résolution du tenant ou le rate limiting, plutôt que de la répéter sur chaque endpoint.
from fastapi import APIRouter, Depends
from .security import require_admin
from .routers import billing, audit
admin = APIRouter(prefix="/admin", dependencies=[Depends(require_admin)])
admin.include_router(billing.router)
admin.include_router(audit.router)Valider les paramètres de chemin et de requête
Les paramètres de chemin et de requête sont validés depuis leurs annotations de type, et Path() et Query() ajoutent des contraintes : ge et le pour les nombres, min_length, max_length et pattern pour les chaînes, plus une valeur par défaut et une description qui alimentent le schéma OpenAPI. Une requête qui casse une contrainte reçoit une réponse 422 automatique avec la liste précise des erreurs, avant même que votre fonction s'exécute.
from fastapi import APIRouter, Path, Query
router = APIRouter(prefix="/orders", tags=["orders"])
@router.get("/{order_id}")
async def get_order(
order_id: int = Path(ge=1),
fields: str | None = Query(default=None, pattern="^[a-z_,]+$"),
limit: int = Query(default=20, ge=1, le=100),
):
...Valider le corps de requête avec Pydantic
Quand un paramètre est un modèle Pydantic, FastAPI lit le corps de la requête dedans et valide chaque champ. Field() porte les contraintes par champ ; field_validator traite un champ unique avec une logique custom ; model_validator s'exécute une fois l'objet entier construit, c'est là que vivent les règles inter-champs.
from pydantic import BaseModel, Field, field_validator
class OrderCreate(BaseModel):
customer_id: int = Field(gt=0)
currency: str = Field(default="EUR", pattern="^[A-Z]{3}$")
quantity: int = Field(gt=0, le=999)
@field_validator("currency")
@classmethod
def known_currency(cls, value: str) -> str:
if value not in {"EUR", "USD", "MGA"}:
raise ValueError("devise non supportée")
return valueFaçonner les réponses : response_model et codes de statut
Un response_model filtre la sortie à travers un schéma : les champs internes ne fuient jamais, même si votre fonction renvoie un objet de base de données complet, et la forme de la réponse est documentée. Associez-le à un status_code explicite, et à response_model_exclude_none quand les champs nullables doivent disparaître plutôt que d'être sérialisés en null.
from fastapi import status
class OrderOut(BaseModel):
id: int
customer_id: int
total: float
@router.post("", response_model=OrderOut, status_code=status.HTTP_201_CREATED)
async def create_order(payload: OrderCreate) -> Order:
return await orders.create(payload) # les champs en trop sont retirés par OrderOutPersonnaliser l'erreur de validation 422
Le corps 422 par défaut est détaillé et lisible par une machine, mais une API publique a souvent besoin de ses erreurs dans une enveloppe unique et cohérente, par exemple un document problem RFC 9457. Surchargez le handler de RequestValidationError : exc.errors() vous donne la liste structurée, vous la reformez sans perdre d'information.
from fastapi import Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def validation_error(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"type": "https://api.example.com/errors/validation",
"title": "Requête invalide",
"status": 422,
"errors": exc.errors(),
},
media_type="application/problem+json",
)Versionner une API FastAPI
Il y a trois façons de versionner. Un préfixe d'URL (/v1, /v2) est le plus courant : un router par version, visible dans les logs, facile à mettre en cache, trivial pour les clients. Le versioning par header garde des URL propres mais masque la version aux caches et aux logs. Le versioning par media type est le plus correct au sens REST et le moins pratique à déboguer.
| Approche | Visible logs / caches | Effort client | Complexité routing |
|---|---|---|---|
| Préfixe d'URL (/v1) | Oui | Faible | Faible, un router par version |
| Header (X-API-Version) | Non | Moyen | Moyen, matching custom |
| Media type (Accept) | Non | Élevé | Élevé |
Pour une API publique, utilisez le préfixe d'URL. Gardez la version précédente en service jusqu'à ce que vos clients aient migré, et annoncez une date de retrait.
Ce qui a changé dans FastAPI en 2026
FastAPI continue de livrer des releases incrémentales plutôt qu'une réécriture 1.0. La version 0.139 (juillet 2026) donne un bon aperçu de la direction : plus de contrôle sur le routing, une validation plus stricte par défaut, et une transition plus douce pour les codebases encore sur Pydantic v1.
- ✓APIRouter gagne matches() et handle() : un router peut décider lui-même s'il traite une requête, ce qui fait du versioning par header un pattern de première classe.
- ✓Les routes ajoutées à un router après son inclusion dans l'app s'enregistrent désormais correctement, au lieu d'être silencieusement ignorées.
- ✓Le JSON entrant est vérifié pour un header Content-Type valide avant parsing, ce qui rejette les requêtes mal étiquetées au lieu de les mal interpréter.
- ✓Importer depuis pydantic.v1 permet aux modèles v1 et v2 de coexister dans une app, transformant une migration big-bang en migration modèle par modèle.
- ✓Python 3.14 est supporté, Python 3.8 est retiré de la CI, et la syntaxe interne cible maintenant 3.9+.
from fastapi import APIRouter, Request
class HeaderVersionedRouter(APIRouter):
def __init__(self, *args, version: str, **kwargs):
super().__init__(*args, **kwargs)
self.version = version
def matches(self, request: Request) -> bool:
return request.headers.get("X-API-Version") == self.version
router_v1 = HeaderVersionedRouter(version="1")
router_v2 = HeaderVersionedRouter(version="2")La vérification stricte du Content-Type est le seul changement de comportement à tester si vos clients sont peu typés. La désactivation se fait via un flag strict_content_type=False par route : à utiliser de façon délibérée pour une intégration legacy, pas comme réglage par défaut global.
FAQ
- Comment structurer les routes d'une grosse app FastAPI ?
- Un APIRouter par ressource ou par domaine, chacun dans son module avec son préfixe et ses tags, assemblés dans main.py avec include_router. Mettez les préoccupations transverses comme l'authentification ou le rate limiting dans des dépendances au niveau du router plutôt que de les répéter sur chaque endpoint.
- Quelle différence entre APIRouter et l'app FastAPI ?
- FastAPI est l'application qui tourne ; APIRouter est un groupe de routes montable, sans serveur propre. Vous construisez des routers dans des modules de fonctionnalité et vous les incluez dans l'app, ou dans un router parent. Ça garde un gros codebase navigable et permet d'appliquer un préfixe et des dépendances par groupe.
- Comment personnaliser l'erreur de validation 422 de FastAPI ?
- Enregistrez un handler d'exception pour RequestValidationError et renvoyez votre propre forme de réponse, par exemple un document problem RFC 9457. exc.errors() vous donne la liste structurée des erreurs, vous la mappez sur votre enveloppe sans perdre de détail.
- Comment versionner une API FastAPI ?
- L'approche la plus courante est un préfixe d'URL (/v1, /v2) avec un router par version : visible dans les logs, facile à mettre en cache, simple pour les clients. Le versioning par header ou par media type garde les URL propres mais est plus difficile à déboguer et à mettre en cache.
- Faut-il migrer de Pydantic v1 à v2 ?
- Oui, mais de façon incrémentale. Depuis 2026, vous pouvez importer depuis pydantic.v1 pour que les modèles v1 et v2 coexistent dans la même app, ce qui transforme une migration big-bang risquée en migration modèle par modèle. La v2 est nettement plus rapide et c'est là qu'arrivent les nouveautés.
Le routing et la validation dans FastAPI récompensent un peu de structure : des routers par domaine, des contraintes sur chaque paramètre, un response_model sur chaque endpoint, et une forme d'erreur cohérente. Faites cela et le framework gère le reste à chaque requête.
Besoin d'aide sur ce sujet ? Conception d'API REST
Découvrir ce service →