Qu’est-ce que FastAPI ?
FastAPI est un framework web Python moderne pour la création d’API. Son concept fondamental est que vos type hints font office de contrat. Déclarez un paramètre comme int, un corps de requête comme un modèle Pydantic, ou un type de retour comme ItemOut, et le framework valide la requête, sérialise la réponse et rédige la documentation — tout cela à partir de cette unique déclaration.
Il repose sur deux bibliothèques dont vous devez retenir le nom. Starlette fournit la boîte à outils ASGI : le routage, le middleware et le cycle requête/réponse. Pydantic s’occupe de la validation et de la sérialisation. FastAPI est la couche qui les combine, ajoute l’injection de dépendances et génère un schéma OpenAPI au démarrage.
Le résultat est qu’une fonction simple et lisible vous offre un endpoint validé avec une documentation interactive. La magie est bien réelle, mais elle est mécanique, et c’est précisément l’objet de ce guide : comprendre ces mécanismes.
Opérations de chemin
Une opération de chemin (path operation) est une fonction liée à une méthode HTTP et à une URL. Le décorateur nomme la méthode, et la signature de la fonction indique à FastAPI ce qu’il doit attendre.
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello, world"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
Le segment {item_id} est un paramètre de chemin, et son annotation int signifie que FastAPI convertit la chaîne de caractères et renvoie automatiquement une erreur 422 s’il n’y parvient pas. Tout élément qui ne figure pas dans le chemin est traité comme un paramètre de requête, et une valeur par défaut de None le rend optionnel. Il n’y a aucun parsing manuel ni aucun request.args.
Groupez les opérations liées à l’aide d’un APIRouter, qui peut comporter un préfixe et des tags pour la documentation.
from fastapi import APIRouter
router = APIRouter(prefix="/items", tags=["items"])
@router.get("")
async def list_items():
return []
@router.post("", status_code=201)
async def create_item():
return {}
Incluez ensuite le router dans l’application : app.include_router(router). C’est ainsi qu’un projet FastAPI reste modulaire.
Modèles Pydantic
Un modèle Pydantic est une classe qui hérite de BaseModel. Il analyse l’entrée, convertit les types compatibles, applique des contraintes et lève une erreur de validation claire lorsqu’un problème survient.
# app/schemas.py
from datetime import datetime
from pydantic import BaseModel, Field
class ItemBase(BaseModel):
name: str = Field(min_length=1, max_length=120)
price: float = Field(gt=0)
tags: list[str] = []
class ItemCreate(ItemBase):
pass
class ItemOut(ItemBase):
id: int
created_at: datetime
model_config = {"from_attributes": True}
Field ajoute des contraintes et des métadonnées. model_config = {"from_attributes": True} permet à Pydantic de lire des attributs depuis un objet ORM, vous permettant ainsi de retourner directement une ligne de base de données et de la voir sérialisée. La séparation entre ItemCreate et ItemOut est délibérée : le modèle d’entrée n’a pas de id, et le modèle de sortie peut exposer des champs que le client ne peut pas définir.
Utilisez un modèle Pydantic comme type pour le corps de la requête et FastAPI l’analysera, le validera et vous remettra un objet typé.
@app.post("/items", response_model=ItemOut, status_code=201)
async def create_item(item: ItemCreate):
return await repo.create(item)
Si le corps est invalide, FastAPI retourne une erreur 422 avec la liste exacte des champs en échec. Vous n’avez pas eu à écrire une seule ligne de validation.
Corps de requête et validation
FastAPI distingue les sources de données par leur position et leur type :
- Les paramètres de chemin (path parameters) proviennent de l’URL.
- Les paramètres de requête (query parameters) correspondent à tout le reste de la signature.
- Un modèle Pydantic représente le corps de la requête (request body).
Header,Cookie,FormetFilemarquent ces entrées spéciales.
from fastapi import Header, Query
@app.get("/search")
async def search(
q: str = Query(min_length=1, max_length=50),
limit: int = Query(default=20, ge=1, le=100),
user_agent: str | None = Header(default=None),
):
return {"q": q, "limit": limit, "user_agent": user_agent}
Les contraintes sur les paramètres de requête sont appliquées de la même manière que sur les modèles, et la documentation générée affiche ces limites. Cette cohérence est la raison pour laquelle le code FastAPI est si compact.
Documentation automatique
Comme le schéma est généré à partir du code, la documentation n’est jamais obsolète. Lancez l’application et ouvrez /docs pour Swagger UI, où vous pouvez appeler les endpoints directement, ou /redoc pour une référence plus épurée.
app = FastAPI(
title="Inventory API",
version="1.0.0",
description="Items, stock levels and orders.",
)
Vous pouvez ajouter des résumés, des descriptions et des exemples aux opérations de chemin et aux champs, et ils apparaîtront dans la documentation. Pour les équipes frontend, ce même schéma peut générer des clients typés, permettant ainsi à l’API et à ses consommateurs de rester synchronisés. C’est tout l’avantage de traiter les types comme un contrat.
Dépendances
Une dépendance est une fonction que FastAPI appelle avant votre endpoint et dont la valeur de retour est injectée. Depends() est le mécanisme, et il est composable.
from fastapi import Depends, HTTPException
async def get_session() -> AsyncSession:
async with SessionLocal() as session:
yield session
async def get_current_user(
token: str = Depends(get_token),
session: AsyncSession = Depends(get_session),
) -> User:
user = await authenticate(session, token)
if user is None:
raise HTTPException(status_code=401, detail="Not authenticated")
return user
@app.get("/me", response_model=UserOut)
async def read_me(user: User = Depends(get_current_user)):
return user
Les dépendances peuvent être synchrones ou asynchrones, peuvent utiliser yield pour exécuter un nettoyage après la réponse, et sont mises en cache au sein d’une requête, sehingga get_session n’est appelé qu’une seule fois même si plusieurs dépendances en ont besoin. Elles constituent l’emplacement idiomatique pour les sessions de base de données, l’authentification, la pagination et les feature flags.
Async versus sync
FastAPI supporte les endpoints def et async def, et ce choix est important.
- Un endpoint
async defs’exécute sur la boucle d’événements (event loop). Chaque appel d’E/S à l’intérieur doit être attendu via une bibliothèque async, sinon il bloque la boucle pour toutes les autres requêtes. - Un endpoint
defclassique est exécuté dans un pool de threads ; les appels bloquants sont donc sans danger, mais chaque requête occupe un thread.
# Async: use an async driver and await it
@app.get("/items")
async def list_items(session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Item))
return result.scalars().all()
# Sync: blocking code is fine; FastAPI offloads it
@app.get("/report")
def build_report():
return generate_report() # CPU or blocking I/O
La règle est simple : tout en async, ou tout en sync. Insérer un appel bloquant dans un endpoint async def est le bug de performance le plus courant avec FastAPI, et il reste invisible jusqu’à l’arrivée du trafic.
Erreurs et codes de statut
Levez une exception HTTPException pour retourner une erreur avec un code de statut et un détail au format JSON. FastAPI la transforme en réponse et documente le code de statut.
from fastapi import HTTPException, status
@app.get("/items/{item_id}", response_model=ItemOut)
async def read_item(item_id: int):
item = await repo.get(item_id)
if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Item not found",
)
return item
Pour garantir une structure d’erreur cohérente sur l’ensemble de l’API, enregistrez des gestionnaires d’exceptions (exception handlers) qui convertissent les erreurs du framework et du domaine en une enveloppe unique. Gardez le champ detail stable ; les clients s’appuieront dessus pour le parsing.
Tâches d’arrière-plan
BackgroundTasks exécute des tâches après l’envoi de la réponse, au sein du même processus. C’est l’option légère pour envoyer un e-mail ou écrire une ligne de journal d’audit.
from fastapi import BackgroundTasks
def send_receipt(email: str) -> None:
...
@app.post("/orders", status_code=201)
async def create_order(
order: OrderCreate,
tasks: BackgroundTasks,
):
saved = await repo.create(order)
tasks.add_task(send_receipt, saved.email)
return saved
Les tâches d’arrière-plan ne sont pas une file d’attente de jobs (job queue). Elles s’exécutent dans le processus, elles sont donc perdues si le worker redémarre et elles entrent en concurrence avec la gestion des requêtes. Pour tout besoin de persistance, déléguez plutôt à Celery, RQ ou à une file d’attente managée.
Bases de données
FastAPI ne possède pas de couche de base de données, vous devez donc choisir la vôtre. Les choix les plus courants sont SQLAlchemy avec un moteur asynchrone ou SQLModel, qui combine SQLAlchemy et Pydantic. Le pattern de dépendance permet de fournir la session et de la fermer après la requête.
# app/db.py
from collections.abc import AsyncIterator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_session() -> AsyncIterator[AsyncSession]:
async with SessionLocal() as session:
yield session
@router.post("", response_model=ItemOut, status_code=201)
async def create_item(
payload: ItemCreate,
session: AsyncSession = Depends(get_session),
):
item = Item(**payload.model_dump())
session.add(item)
await session.commit()
await session.refresh(item)
return item
Utilisez expire_on_commit=False pour que les objets restent utilisables après le commit, et gardez les transactions à l’intérieur de la requête. Si vous préférez un driver synchrone, utilisez des endpoints def classiques afin que FastAPI puisse les déléguer au pool de threads.
Sécurité
FastAPI fournit les briques de base pour OAuth2 et JWT sans imposer de système d’authentification complet. OAuth2PasswordBearer extrait le jeton, et python-jose ou pyjwt le vérifie.
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/users/me", response_model=UserOut)
async def read_me(
token: str = Depends(oauth2_scheme),
session: AsyncSession = Depends(get_session),
):
user = await get_user_from_token(session, token)
if user is None:
raise HTTPException(status_code=401, detail="Invalid token")
return user
Stockez les hachages de mots de passe avec passlib ou argon2-cffi, jamais en texte brut. Effectuez le hachage et la vérification en dehors du point de terminaison, conservez vos secrets dans les paramètres, et configurez avec soin la limitation de débit (rate limiting) et le CORS. FastAPI vous donne les outils ; c’est à vous de définir la politique de sécurité.
Configuration et paramètres
Regroupez votre configuration dans un seul objet typé au lieu de lire les variables d’environnement partout dans le code. pydantic-settings valide l’environnement au démarrage et vous offre la même sécurité de typage que vos modèles.
# app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_")
database_url: str
secret_key: str
access_token_minutes: int = 30
settings = Settings()
from functools import lru_cache
from app.config import Settings
@lru_cache
def get_settings() -> Settings:
return Settings()
@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
return {"token_minutes": settings.access_token_minutes}
Comme les paramètres sont injectés comme n’importe quelle autre dépendance, les tests peuvent les surcharger avec app.dependency_overrides[get_settings]. L’absence de variables obligatoires provoque un échec au démarrage plutôt qu’à la première requête, ce qui est précisément le moment où vous voulez le savoir.
Tests
TestClient enveloppe l’application dans un client de type WSGI et effectue de véritables requêtes au sein du processus, incluant la validation et le remplacement des dépendances.
# tests/test_items.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_create_item():
response = client.post("/items", json={"name": "Cup", "price": 9.5})
assert response.status_code == 201
assert response.json()["name"] == "Cup"
def test_invalid_price():
response = client.post("/items", json={"name": "Cup", "price": -1})
assert response.status_code == 422
Utilisez app.dependency_overrides pour remplacer la session de base de données par une session de test, et rédigez des tests asynchrones avec httpx.AsyncClient lorsque vous devez tester directement des endpoints asynchrones. Comme les dépendances sont explicites, les remplacer dans les tests est très simple.
Bonnes pratiques
- Déclarez explicitement les modèles de requête et de réponse ; n’acceptez et ne retournez jamais de dictionnaires bruts.
- Gardez vos endpoints
async defentièrement async, ou utilisezdefpour que FastAPI délègue les tâches bloquantes. - Placez la logique partagée dans des dépendances
Depends()plutôt que de la répéter pour chaque route. - Segmentez l’application avec
APIRouteret attribuez à chaque router un préfixe et des tags. - Ajoutez des contraintes avec
FieldetQueryplutôt que de valider manuellement. - Levez des
HTTPExceptionavec le code de statut approprié et undetailstable. - Surchargez les dépendances dans vos tests au lieu de patcher les composants internes.
- Conservez vos secrets dans des objets de configuration lus depuis l’environnement.
Erreurs courantes
- Appeler du code bloquant à l’intérieur d’un endpoint
async def, ce qui bloque l’event loop. - Retourner des objets ORM sans
response_model, exposant ainsi des colonnes internes. - Oublier
from_attributes = Trueet se demander pourquoi la sérialisation échoue. - Écrire un seul
main.pygéant au lieu d’utiliser des routers et des modules. - Utiliser
BackgroundTaskscomme une file d’attente de tâches (job queue) durable. - Partager une seule session entre plusieurs requêtes ou tâches asynchrones.
- Valider deux fois la même entrée parce que le modèle Pydantic a été ignoré.
- Capturer toutes les exceptions et retourner un code 200 avec un corps d’erreur.
Et après ?
FastAPI transforme les indices de type (type hints) en une API validée et documentée, c’est pourquoi il est devenu le choix par défaut pour les nouveaux services Python. Pour comparer les compromis, consultez le guide Flask pour l’approche synchrone minimale et celui de Django pour un framework complet incluant un ORM et une interface d’administration. Approfondissez ensuite la notion de contrat d’API avec OpenAPI et les règles de conception des [REST APIs](/guides/rest], et assurez-vous que vos endpoints méritent d’être documentés en définissant correctement votre modèle de ressources.