backend / fastapi
Dependency Injection
Explication
Ce que vous allez apprendre
- Déclarer une dépendance réutilisable avec
Depends()et l'injecter dans plusieurs endpoints - Utiliser
yielddans une dépendance pour garantir un teardown fiable (fermeture de session DB) - Chaîner des dépendances qui dépendent elles-mêmes d'autres dépendances
- Appliquer une dépendance à toutes les routes d'un router avec
dependencies=[...] - Comprendre pourquoi une dépendance n'est évaluée qu'une seule fois par requête
Dans quel contexte ?
Douze endpoints différents dans app/routers/ ont besoin d'une session de base de données ouverte au début de la requête et fermée à la fin, même en cas d'erreur. Sans dependency injection, chaque endpoint devrait ouvrir et fermer sa propre session manuellement, avec le risque d'oublier le finally: db.close() dans un cas d'exception. Une seule fonction get_db() avec yield, injectée via Depends(get_db), centralise cette logique et garantit qu'aucune session ne reste ouverte par erreur.
Une image pour commencer
Le terme "injection de dépendances" sonne compliqué, mais l'idée est simple. C'est un peu comme commander un plat au restaurant : tu déclares ce que tu veux, tu n'as pas besoin de savoir comment la cuisine le prépare.
Concrètement, au lieu qu'une fonction aille elle-même chercher tout ce dont elle a besoin, elle DÉCLARE ce dont elle a besoin. Un mécanisme extérieur, ici FastAPI, le lui fournit automatiquement avant qu'elle ne s'exécute.
Pourquoi ce mécanisme est-il si puissant ? Sans lui, chaque endpoint qui a besoin d'une session de base de données ou de l'utilisateur connecté devrait dupliquer ce code de récupération.
Avec Depends(), cette logique est écrite UNE fois et réutilisée partout. Ça réduit la duplication et centralise les changements futurs à un seul endroit.
Une fois ce principe compris, un détail syntaxique mérite une attention particulière : le rôle de yield. Une dépendance qui l'utilise au lieu de return sépare le code en deux parties.
Tout ce qui est AVANT le yield s'exécute avant l'endpoint, comme ouvrir une connexion. Tout ce qui est APRÈS s'exécute après, comme la fermer — et ce, MÊME SI l'endpoint lève une exception.
C'est exactement ce mécanisme qui garantit qu'une session de base de données est toujours proprement fermée. Peu importe ce qui arrive pendant le traitement de la requête.
Il reste un dernier cas à connaître : les dépendances qui en appellent d'autres. get_current_user peut avoir besoin de get_current_token, par exemple.
FastAPI résout automatiquement toute cette chaîne. Et met en cache le résultat d'une même dépendance appelée plusieurs fois au sein d'une seule requête, pour éviter un travail redondant.
Bonne pratique
Utilise toujours yield plutôt que return pour une dépendance qui ouvre une ressource (session DB, connexion, fichier) : le code après le yield s'exécute même si l'endpoint lève une exception, ce qu'un simple return ne garantit jamais.
| Type de dépendance | Syntaxe | Cas d'usage |
|---|---|---|
| Simple (pas de teardown) | return valeur | Pagination, configuration |
| Avec ressource à libérer | yield valeur | Session DB, connexion réseau |
| Appliquée à tout un router | dependencies=[Depends(...)] | Authentification obligatoire sur un groupe de routes |
Et la suite ? Ce mécanisme est la fondation de presque tout ce qui suivra dans ce cours : l'accès à la base de données, l'authentification et les permissions reposent tous sur Depends().
Commandes & code
Dependency Injection
from fastapi import FastAPI, Depends
from typing import Annotated
app = FastAPI()
# Une dépendance simple : une fonction réutilisable par plusieurs endpoints
def get_pagination_params(page: int = 1, page_size: int = 20):
return {"page": page, "page_size": page_size}
@app.get("/products")
def list_products(pagination: Annotated[dict, Depends(get_pagination_params)]):
return {"pagination": pagination, "products": []}
@app.get("/orders")
def list_orders(pagination: Annotated[dict, Depends(get_pagination_params)]):
# même dépendance réutilisée, aucune duplication de la logique de pagination
return {"pagination": pagination, "orders": []}# Dépendances basées sur une classe — pratique quand la config a plusieurs paramètres
class Paginator:
def __init__(self, page: int = 1, page_size: int = 20, max_page_size: int = 100):
self.page = page
self.page_size = min(page_size, max_page_size) # protège contre un abus (page_size=999999)
self.offset = (page - 1) * self.page_size
@app.get("/v2/products")
def list_products_v2(paginator: Annotated[Paginator, Depends()]):
return {"offset": paginator.offset, "limit": paginator.page_size}# Dépendances avec yield — gestion de ressources (setup / teardown garanti)
from sqlalchemy.orm import Session
from app.core.database import SessionLocal
def get_db():
db = SessionLocal()
try:
yield db # la session est utilisable dans l'endpoint
finally:
db.close() # TOUJOURS exécuté, même si l'endpoint lève une exception
@app.get("/users/{id}")
def get_user(id: int, db: Annotated[Session, Depends(get_db)]):
return db.query(User).filter(User.id == id).first()# Sous-dépendances — une dépendance peut elle-même dépendre d'une autre
def get_current_token(authorization: str = ""):
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Token manquant")
return authorization.removeprefix("Bearer ")
def get_current_user(
token: Annotated[str, Depends(get_current_token)],
db: Annotated[Session, Depends(get_db)],
):
user = decode_token_and_find_user(token, db)
if not user:
raise HTTPException(status_code=401, detail="Token invalide")
return user
@app.get("/me")
def read_current_user(user: Annotated["User", Depends(get_current_user)]):
return user# Dépendances au niveau du router entier — appliquées à TOUTES ses routes
from fastapi import APIRouter
admin_router = APIRouter(
prefix="/admin",
dependencies=[Depends(get_current_user)], # vérifie l'auth sur CHAQUE route de ce router
)
@admin_router.get("/stats")
def admin_stats():
return {"visits": 4231} # get_current_user est déjà exécuté avant d'arriver ici# Dépendances mises en cache par requête — évaluées UNE seule fois même si utilisées plusieurs fois
def get_settings():
print("Chargement de la config...") # ne s'affiche qu'une fois par requête
return {"debug": False}
@app.get("/cached-demo")
def demo(
settings1: Annotated[dict, Depends(get_settings)],
settings2: Annotated[dict, Depends(get_settings)], # réutilise le résultat déjà calculé
):
return settings1 is settings2 # TrueRésumé
Depends()injecte une valeur calculée par une fonction, réutilisable sur plusieurs endpoints.yielddans une dépendance garantit un teardown (fermeture DB, libération de ressource) même en cas d'erreur.- Les dépendances peuvent en dépendre d'autres (
get_current_userdépend deget_current_token). dependencies=[Depends(...)]au niveau d'unAPIRouterapplique une vérification à toutes ses routes d'un coup.
Exercices pratiques
Mission : les sessions de base de données qui ne se ferment jamais
Objectif : Corriger une dépendance de session DB sans teardown fiable et sécuriser un router admin entier avec une dépendance d'authentification.
Contexte
Dans app/core/database.py, la fonction get_db() ouvre une session avec return SessionLocal() sans jamais la fermer explicitement. En production, le monitoring signale une fuite progressive de connexions à la base, surtout quand certains endpoints lèvent une exception avant la fin du traitement. Par ailleurs, app/routers/admin.py vérifie l'authentification manuellement dans chacune de ses douze routes.