Abstractions qui fuient¶
Anti-pattern. Le framework HTTP (FastAPI, Litestar, ...) fuit dans le code métier. Migrer ou tester sans le framework devient impossible.
Symptômes¶
# ❌ Le métier importe FastAPI
from fastapi import HTTPException
class UserService:
def get(self, user_id: int) -> User:
user = self._session.get(User, user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
# ❌ Les handlers utilisent Depends pour de la logique métier
from fastapi import Depends
@app.get("/me")
def me(current_user: User = Depends(get_current_user)):
return current_user.dict()
# ❌ Les schémas Pydantic héritent de classes du framework
from fastapi import UploadFile
class CreateProfilePayload(BaseModel):
avatar: UploadFile # type: ignore — couplé à FastAPI
Pourquoi c'est tentant¶
Le framework est généreux : HTTPException au lieu d'écrire votre
exception, Depends au lieu d'écrire votre injection, UploadFile au
lieu d'écrire votre type. Économie de 5 lignes par cas. Multiplié par
200 endpoints…
Pourquoi c'est mauvais¶
Migration framework = réécriture totale¶
Vous voulez essayer Litestar ? HTTPException n'existe pas en Litestar.
Depends n'existe pas. UploadFile n'existe pas. Tout votre code
métier doit changer.
Tests couplés¶
Pour tester UserService.get, il faut une app FastAPI montée, parce que
HTTPException n'a de sens qu'avec le middleware FastAPI. Vous montez
une mini-app pour chaque test.
Frontière brouillée¶
Avec ces fuites, où s'arrête le métier, où commence le HTTP ? Personne ne sait. Le code dérive jusqu'à ce que tout soit framework.
Le bon motif¶
# ✅ Le métier ne sait rien du framework
from mic.model.errors import DomainError
class UserService:
def get(self, user_id: int) -> User:
user = self._session.get(User, user_id)
if user is None:
raise DomainError(
code="users.not_found",
message=f"User id={user_id} not found",
details={"user_id": user_id},
)
return user
# ✅ Les handlers retournent un type neutre
def _read_user(*, user_id: str) -> HttpResponseSpec:
with session_factory() as session:
service = UserService(session=session)
user = service.get(int(user_id))
return HttpResponseSpec(
status_code=200,
body=wrap_http_success(_serialize(user)),
)
L'adaptateur (FastAPI ou Litestar) fait le mappage :
DomainError→ 400 + corps canoniqueTransientError→ 503 + corps canoniqueHttpResponseSpec(status_code=200, body=...)→Response(status_code=200, content=...)
La règle d'or¶
Si vous écrivez from fastapi import ... ou from litestar import ...
dans un fichier qui n'est PAS app.py, vous laissez fuiter le framework.
Deux exceptions :
app.py(factory de l'app) — c'est le seul fichier qui assemble.- Endpoints d'infra purs (
/metrics,/healthnatifs au framework si vraiment besoin) — pas de logique métier dedans.
La preuve qu'on tient¶
Le test tests/http/test_adapter_parity.py exécute les mêmes tests
deux fois — une fois sous FastAPI, une fois sous Litestar. Si une
abstraction fuit, l'un des deux cassera.