Skip to content

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 canonique
  • TransientError → 503 + corps canonique
  • HttpResponseSpec(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, /health natifs 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.