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.