Aller au contenu

2. Framework-agnostique

Règle. Aucune ligne de code métier (mail/, audit/, search/, ...) n'importe fastapi ou litestar. Les routes sont des HttpRouteSpec purs 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 :

from mic.fastapi import build_fastapi_router
from mic.litestar import build_litestar_router

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,
    ),
)
  1. 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) :

  1. extrait les query / path / body / headers de la requête native,
  2. appelle le handler avec les bons kwargs,
  3. récupère le HttpResponseSpec,
  4. sérialise en Response native 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.