2. Framework-agnostique¶
Règle. Aucune ligne de code métier (
mail/,audit/,search/, ...) n'importefastapioulitestar. Les routes sont desHttpRouteSpecpurs servis par des adaptateurs interchangeables.
Pourquoi¶
Le framework HTTP est une commodité qui change avec le temps :
- Flask → FastAPI → ?
- Sync → async → ?
- Mono-process → ASGI → ?
Si vos handlers métier importent fastapi.HTTPException, fastapi.Depends,
ou utilisent @app.get(...), votre métier est lié à FastAPI pour la vie.
Migrer demande une réécriture.
MIC matérialise la frontière en code : les handlers métier ne touchent
que des types de la lib (HttpRouteSpec, HttpResponseSpec,
HttpRequestContext, DomainError).
La preuve : 11 tests × 2 frameworks¶
mic-struct livre deux adaptateurs :
Et 11 tests paramétrés s'exécutent deux fois, une fois sous chaque
adaptateur (tests/http/test_adapter_parity.py). Si une route se comporte
différemment entre FastAPI et Litestar, c'est une fuite d'abstraction
détectée immédiatement.
@pytest.fixture(params=["fastapi", "litestar"])
def client(request):
if request.param == "fastapi":
return _build_fastapi_client()
return _build_litestar_client()
def test_domain_error_maps_to_400(client):
response = client.get("/biz-error")
assert response.status_code == 400
assert response.json()["code"] == "biz.refused"
Anatomie d'un handler agnostique¶
from mic.contracts import StrictSchema, wrap_http_success
from mic.http import HttpResponseSpec, HttpRouteSpec
class CreateItem(StrictSchema):
name: str
quantity: int = 0
def _create(*, payload: CreateItem) -> HttpResponseSpec: # (1)
# Pas d'import fastapi / litestar ici
return HttpResponseSpec(
status_code=201,
body=wrap_http_success({"name": payload.name}),
)
routes = (
HttpRouteSpec(
method="POST",
path="/api/v1/items",
name="items_v1_create",
category="items",
handler=_create,
),
)
- Le handler reçoit ses entrées en kwargs typés :
payload(Pydantic),path_param_name(str),ctx(HttpRequestContext). L'adaptateur résout ce qu'il faut depuis la requête.
L'adaptateur fait le pont¶
L'adaptateur (FastAPI ou Litestar) :
- extrait les query / path / body / headers de la requête native,
- appelle le handler avec les bons kwargs,
- récupère le
HttpResponseSpec, - sérialise en
Responsenative du framework.
Le handler ne sait jamais comment l'adaptateur fait son travail.
Les exceptions à la règle¶
Deux endroits où l'on autorise du code natif au framework :
app.py (factory de l'app)¶
C'est le seul fichier qui assemble l'app. Il importe FastAPI pour la
construire, mais ne contient pas de logique métier.
from fastapi import FastAPI
from mic.fastapi import build_fastapi_router
app = FastAPI()
app.include_router(build_fastapi_router(routes=all_routes()))
Endpoints d'infra (/metrics)¶
/metrics retourne des bytes Prometheus, pas du JSON enveloppé. C'est
de l'infra, pas du métier — on l'attache directement au framework :
@app.get("/metrics", include_in_schema=False)
def metrics() -> Response:
return Response(content=render_metrics(), media_type="text/plain; version=0.0.4")
C'est OK : /metrics n'a pas de logique métier, et il n'y a aucun bénéfice
à le passer par MIC.
Le test qui vous protège¶
Si vous ajoutez un handler qui importe fastapi, il marchera côté FastAPI
mais cassera côté Litestar. Le test_adapter_parity.py doit aussi servir
votre nouvelle route sur la matrice des deux frameworks. Si vous ne l'ajoutez
pas, vous trichez avec votre futur vous-même.