Aller au contenu

Migrer un service existant

Vous avez déjà un service FastAPI / Litestar et vous voulez adopter MIC sans réécriture massive. Voici la trajectoire progressive validée.

Phase 1 — Adopter StrictSchema + DomainError

Coût : 1 à 2 jours. Bénéfice : 80 % du gain sécurité / contrats.

# Avant
class CreateUser(BaseModel):
    email: str
    name: str = ""

@app.post("/users")
def create_user(payload: CreateUser):
    if "@" not in payload.email:
        raise HTTPException(400, "Invalid email")
    ...

# Après
from mic.contracts import StrictSchema
from mic.model.errors import DomainError

class CreateUser(StrictSchema):     # ← extra="forbid", frozen=True
    email: str
    name: str = ""

def _create_user(*, payload: CreateUser):
    if "@" not in payload.email:
        raise DomainError(
            code="users.invalid_email",
            message="email must contain @",
            details={"received_email": payload.email},
        )
    ...

Gain immédiat : champs supplémentaires rejetés, vulnérabilité d'assignation de masse fermée, codes d'erreur stables côté client.

Phase 2 — Extraire les services métier

Coût : 1 semaine selon la taille. Bénéfice : testabilité, frontière nette.

# Avant
@app.post("/users")
def create_user(payload: CreateUser, session: Session = Depends(get_session)):
    user = User(**payload.dict())
    session.add(user)
    session.commit()
    return user

# Après
class UserService:
    def __init__(self, *, session: Session) -> None:
        self._session = session

    def create(self, *, email: str, name: str) -> User:
        # Validations métier ici
        user = User(email=email, name=name)
        self._session.add(user)
        self._session.commit()
        self._session.refresh(user)
        return user


def _create_user(*, payload: CreateUser, ctx: HttpRequestContext):
    with session_factory() as session:
        service = UserService(session=session)
        user = service.create(email=payload.email, name=payload.name)
        return HttpResponseSpec(
            status_code=201,
            body=wrap_http_success(_serialize(user)),
        )

Phase 3 — Passer aux HttpRouteSpec

Coût : selon le nombre d'endpoints. Bénéfice : framework-agnostique + parité testée.

# Avant : décorateurs FastAPI
@app.post("/users", status_code=201)
def create_user(...): ...

@app.get("/users/{user_id}")
def read_user(...): ...

# Après : tuple HttpRouteSpec
def user_routes(*, session_factory) -> tuple[HttpRouteSpec, ...]:
    def _create(*, payload: CreateUser): ...
    def _read(*, user_id: str): ...
    return (
        HttpRouteSpec(method="POST", path="/users", name="users.create",
                      category="users", handler=_create),
        HttpRouteSpec(method="GET", path="/users/{user_id}", name="users.read",
                      category="users", handler=_read),
    )

app.include_router(build_fastapi_router(routes=user_routes(...)))

Phase 4 — Remplacer le conteneur DI

Coût : variable selon l'usage. Bénéfice : simplicité, lisibilité.

Voir DI explicite pour le motif de remplacement. La règle de pouce : factory + closures, pas de @inject.

Phase 5 — Tests miroir 100 %

Coût : grand au démarrage, faible à la maintenance. Bénéfice : confiance en prod.

Voir tests miroir. Motif : faire progresser la couverture de 60 % → 80 % → 100 % par module, pas en bloc. À chaque module qui atteint 100 %, ajoutez le gate correspondant pour ne pas régresser.

Quand ne pas migrer

  • Service en fin de vie dans 6 mois → ne perdez pas votre temps.
  • Service monolithique > 50k lignes → la migration coûtera plus que les gains. Mieux vaut le découper en services MIC neufs.
  • Équipe non convaincue par la couverture à 100 % → ne migrez pas la forme, vous casserez le fond.