Middleware avec FastAPI
Ce chapitre vient après Gestion d’erreurs avec FastAPI. Il montre comment ajouter une couche transversale qui s’exécute automatiquement autour de toutes les requêtes HTTP.
Introduction
Jusqu’ici, on a appris à :
- construire une API de base,
- ajouter l’authentification,
- relier les données aux utilisateurs,
- centraliser la gestion d’erreurs.
Il manque maintenant une autre brique importante de maturité :
la capacité à observer et enrichir toutes les requêtes sans copier du code dans chaque route.
C’est exactement le rôle du middleware.
Ce que ce chapitre va accomplir
À la fin de ce chapitre, tu auras :
- un fichier
core/middleware.py, - un middleware HTTP branché dans
main.py, - un
request_idgénéré automatiquement, - un header
X-Request-ID, - un header
X-Process-Time, - un logging propre des appels HTTP,
- une compréhension claire de la différence entre :
- route,
- dépendance,
- handler d’erreur,
- middleware.
Pourquoi ce chapitre vient maintenant
Le middleware devient vraiment intéressant quand ton API commence à avoir :
- plusieurs routes,
- de l’authentification,
- des erreurs métier,
- des comportements globaux qu’on ne veut pas répéter.
Par exemple, tu peux vouloir :
- mesurer le temps de traitement,
- ajouter un identifiant de requête,
- logger les appels,
- enrichir les réponses avec des headers,
- préparer une future observabilité plus avancée.
C’est exactement le bon moment pour l’introduire.
Source d’inspiration réelle
Ce chapitre s’appuie principalement sur :
27-Middleware/135-main.py
Ce que la source montre :
- une mesure du temps de traitement avec
perf_counter, - un middleware HTTP,
- l’idée d’un logging automatique des requêtes.
Choix pédagogique de ce chapitre
La source a une bonne intuition :
- mesurer le temps,
- intercepter globalement les appels,
- journaliser le résultat.
Mais pour notre wiki, on va faire une version un peu plus propre et plus réutilisable :
- on garde l’idée du timing,
- on garde l’idée du logging,
- on ajoute un
request_id, - on sort le middleware de
main.pypour le placer danscore/middleware.py.
Pourquoi ? Parce que cela rend l’architecture plus lisible.
La vraie idée à comprendre
Un middleware n’est pas une route. Un middleware n’est pas non plus une dépendance métier.
Un middleware est une couche transversale qui s’exécute :
- avant que la requête n’atteigne la route,
- puis après que la route a produit une réponse.
Autrement dit, c’est un endroit idéal pour gérer les préoccupations globales.
Ce qu’un middleware fait bien
Un middleware est très adapté pour :
- logging global,
- timing,
- ajout de headers,
- request ID,
- observabilité,
- parfois CORS ou proxy handling via d’autres middlewares spécialisés.
Ce qu’un middleware ne doit pas porter seul
Un middleware n’est pas le meilleur endroit pour :
- la logique métier métier pure,
- la validation métier fine,
- les permissions détaillées par ressource,
- les règles de domaine complexes.
Ces choses vivent mieux dans :
- les services,
- les dépendances,
- ou les handlers d’erreur.
Différence entre middleware, dépendance et handler d’erreur
Dépendance
Elle sert à fournir quelque chose à une route. Exemple : l’utilisateur courant, une session DB, un service.
Handler d’erreur
Il sert à transformer une exception en réponse API cohérente.
Middleware
Il sert à envelopper tout le cycle HTTP. Exemple : mesurer le temps total, logger, ajouter des headers.
Architecture visée après ce chapitre
On ajoute un nouveau fichier :
fastapi-base-api/
├── main.py
├── config.py
├── requirements.txt
├── .gitignore
├── .env
├── .env.example
├── core/
│ ├── __init__.py
│ ├── security.py
│ ├── exceptions.py
│ └── middleware.py
├── api/
│ ├── __init__.py
│ ├── router.py
│ ├── dependencies.py
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── task.py
│ │ └── user.py
│ └── schemas/
│ ├── __init__.py
│ ├── task.py
│ └── user.py
├── database/
│ ├── __init__.py
│ ├── models.py
│ └── session.py
└── services/
├── __init__.py
├── task.py
└── user.py
Repères utiles avant le code
Avant de coder, retiens ceci :
- le middleware voit toutes les requêtes,
- il peut modifier la réponse,
- il peut écrire dans
request.state, - il peut ajouter des headers,
- il ne remplace pas les handlers d’erreur,
- il complète la couche de robustesse.
Étape 1 — créer core/middleware.py
On va écrire un middleware simple et utile :
- il génère un
request_id, - il mesure le temps de traitement,
- il ajoute deux headers à la réponse,
- il écrit un log lisible.
import logging
from time import perf_counter
from uuid import uuid4
from fastapi import Request, Response
logger = logging.getLogger("task_api.middleware")
if not logger.handlers:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)s | %(message)s",
)
async def request_metrics_middleware(request: Request, call_next):
request_id = str(uuid4())
request.state.request_id = request_id
start = perf_counter()
try:
response: Response = await call_next(request)
except Exception:
process_time = round(perf_counter() - start, 4)
logger.exception(
"%s %s -> unhandled exception (%.4fs) [request_id=%s]",
request.method,
request.url.path,
process_time,
request_id,
)
raise
process_time = round(perf_counter() - start, 4)
response.headers["X-Request-ID"] = request_id
response.headers["X-Process-Time"] = str(process_time)
logger.info(
"%s %s -> %s (%.4fs) [request_id=%s]",
request.method,
request.url.path,
response.status_code,
process_time,
request_id,
)
return response
Pourquoi cette version est bonne pédagogiquement
Parce qu’elle montre quatre idées importantes d’un coup :
- le middleware voit la requête avant et après la route
request.statepermet de stocker des infos utiles pendant le cycle de vie de la requête- la réponse peut être enrichie avec des headers
- le log peut être centralisé sans polluer les routes
Étape 2 — brancher le middleware dans main.py
Maintenant, on l’enregistre dans l’application.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from scalar_fastapi import get_scalar_api_reference
from api.router import router
from core.exceptions import add_exception_handlers
from core.middleware import request_metrics_middleware
from database.session import create_db_tables
@asynccontextmanager
async def lifespan(app: FastAPI):
await create_db_tables()
yield
app = FastAPI(
title="Task Organizer API",
description="Une API FastAPI avec middleware HTTP",
lifespan=lifespan,
)
add_exception_handlers(app)
app.middleware("http")(request_metrics_middleware)
app.include_router(router)
@app.get("/scalar", include_in_schema=False)
def get_scalar_docs():
return get_scalar_api_reference(
openapi_url=app.openapi_url,
title="Scalar API",
)
Ce qu’il faut remarquer
La ligne la plus importante est :
app.middleware("http")(request_metrics_middleware)
C’est elle qui dit à FastAPI :
- applique cette fonction à tout le trafic HTTP.
Étape 3 — améliorer l’intégration avec la gestion d’erreurs
Le middleware crée un request_id.
C’est très utile si les réponses d’erreur peuvent aussi renvoyer cet identifiant. Ainsi, côté client ou dans les logs, on peut rattacher un problème à une requête précise.
Tu peux donc enrichir core/exceptions.py comme ceci :
def _build_handler(exception_class: type[AppError]):
async def handler(request: Request, exc: AppError):
content = {"detail": exception_class.detail}
request_id = getattr(request.state, "request_id", None)
if request_id is not None:
content["request_id"] = request_id
return JSONResponse(
status_code=exception_class.status_code,
content=content,
)
return handler
Pourquoi c’est utile
Quand une requête échoue, le client peut recevoir quelque chose comme :
{
"detail": "Task not found",
"request_id": "3de3d824-7d5f-48d4-a1d9-18ac4d3fd31a"
}
Et dans les logs, tu retrouveras ce même request_id.
C’est un très bon premier niveau d’observabilité.
Étape 4 — ne pas déplacer la logique métier dans le middleware
C’est une tentation fréquente.
Exemple d’erreur de design :
- faire toute l’auth dans le middleware,
- décider dans le middleware qui peut modifier quelle tâche,
- mettre des règles métier complexes dans la couche globale.
Pour notre projet, ce n’est pas ce qu’on veut.
On garde :
- l’auth dans
core/security.py+ dépendances, - les règles métier dans
services/, - les erreurs dans
core/exceptions.py, - les comportements transversaux dans
core/middleware.py.
Étape 5 — observer le comportement sur une route normale
Relance l’application :
uvicorn main:app --reload
Puis appelle une route simple :
curl -i http://127.0.0.1:8000/scalar
Tu dois voir apparaître dans les headers quelque chose comme :
X-Request-ID: 5dc8f0f2-1f19-4c1a-a861-2d2fa1f2d8ad
X-Process-Time: 0.0041
Étape 6 — observer le comportement sur une route protégée
Appelle une route protégée avec un token valide :
curl -i http://127.0.0.1:8000/task/mine \
-H "Authorization: Bearer $TOKEN_ENES"
Tu dois là aussi voir :
X-Request-IDX-Process-Time
Étape 7 — observer le comportement sur une erreur
Appelle volontairement une route qui renverra une erreur gérée :
curl -i "http://127.0.0.1:8000/task/?id=999" \
-H "Authorization: Bearer $TOKEN_ENES"
Si tu as enrichi le handler d’erreur avec request_id, tu devrais obtenir un JSON du style :
{
"detail": "Task not found",
"request_id": "..."
}
Et dans les logs, tu verras aussi l’appel correspondant.
Étape 8 — comprendre le lien avec la source 27-Middleware
La source montrait déjà l’idée essentielle :
- mesurer le temps,
- logger les appels,
- utiliser un middleware HTTP.
Ici, on a gardé cette logique, mais on l’a améliorée pour le wiki :
- architecture plus propre,
- middleware externalisé,
request_id,- intégration plus claire avec le chapitre précédent sur les erreurs.
Étape 9 — penser à l’évolution future
Le middleware qu’on vient d’écrire est volontairement simple.
Mais il ouvre naturellement la porte vers :
- corrélation de logs,
- audit trail,
- métriques,
- tracing distribué,
- logs structurés,
- intégration future avec des outils d’observabilité.
Pièges et erreurs fréquentes
- mettre de la logique métier dans le middleware
- croire qu’un middleware remplace les dépendances
- croire qu’un middleware remplace les handlers d’erreur
- oublier que le middleware s’exécute sur toutes les routes
- logger trop d’informations sensibles
- ajouter trop de complexité trop tôt
Bonnes pratiques à retenir
- garder le middleware simple et transversal
- l’utiliser pour les comportements globaux
- enrichir la réponse avec des headers utiles
- utiliser
request.statepour faire circuler des infos techniques - relier le middleware à la gestion d’erreurs pour obtenir une API plus observable
Variantes et évolutions possibles
Après ce chapitre, tu peux aller vers :
- middleware de sécurité plus spécialisés,
- logs JSON structurés,
- ajout d’un
request_iddans tous les logs applicatifs, - intégration avec Celery et traçabilité des jobs,
- instrumentation plus avancée.
Checklist de validation
Avant de passer à la suite, vérifie que tu peux :
- expliquer ce qu’un middleware doit faire et ne pas faire
- ajouter un
request_idsur toutes les requêtes - exposer un temps de traitement dans un header
- utiliser
request.statepour faire circuler une information technique - expliquer pourquoi la logique métier ne doit pas vivre ici
Résumé final
Dans ce chapitre, tu as appris à ajouter une vraie couche transversale à ton API.
Tu as vu comment :
- écrire un middleware HTTP,
- mesurer le temps de traitement,
- générer un
request_id, - enrichir les réponses avec des headers,
- relier le middleware à la gestion d’erreurs.
Si tu retiens une seule chose, retiens celle-ci :
une route gère une action métier ; un middleware observe et enrichit tout le trafic autour de cette action.
Pour aller plus loin
- Parcours de lecture — pour replacer ce chapitre dans la progression globale
- FAQ, erreurs fréquentes et conseils pratiques — pour retrouver les pièges liés aux couches transversales
- Advanced FastAPI — pour garder la vue d’ensemble du bloc avancé
- Tests avec FastAPI — pour verrouiller ensuite automatiquement auth, ownership, erreurs et comportements API