← Engineering posts
May 17, 2026

Why we chose Litestar over FastAPI for RISN AI

Why we chose Litestar over FastAPI for RISN AI

We did not move from FastAPI to Litestar because FastAPI is a poor framework. FastAPI is still a productive way to build Python APIs. It popularized a clean type-hint-first development model, automatic OpenAPI generation, and a dependency injection style that many Python teams now treat as normal.

Our reason was narrower and more practical: the RISN AI backend had grown past the shape that FastAPI makes easiest. We needed a framework that made app composition explicit, kept dependencies scoped at the router boundary, treated middleware as first-class ASGI plumbing, and gave us a test client that matched the async runtime we run in production.

The backend we were actually building

RISN AI is not a CRUD API with a queue attached. The API coordinates magic-link auth, subscription and credit authorization, Redis-backed rate limits, MongoDB operational data through Beanie, ClickHouse analytics, Airflow-triggered research jobs, AI visit ingestion, PostHog exception capture, and a CopilotKit agent surface mounted as a separate ASGI application.

That mix puts pressure on the framework in places that are easy to ignore in a demo: where dependencies live, how middleware order is expressed, whether tests exercise the same async path as production, and how cleanly another ASGI app can be mounted without turning the main API into glue code.

Composition moved to the app boundary

In FastAPI, the common pattern is to build local routers and include them into the app. Litestar pushes us toward a more declarative application shape: route handlers, routers, middleware, exception handlers, CORS, plugins, and lifespan are all visible where the app is created.

app = Litestar(
    route_handlers=[
        *health.router,
        *authorization.router,
        payments_router,
        protected_router,
        copilotkit_handler,
    ],
    plugins=[_structlog_plugin],
    cors_config=CORSConfig(...),
    middleware=[
        DefineMiddleware(RequestContextMiddleware),
        DefineMiddleware(VisitCORSMiddleware),
        DefineMiddleware(DemoReadOnlyMiddleware),
        DefineMiddleware(DemoCacheMiddleware),
        DefineMiddleware(DemoRateLimitMiddleware),
    ],
    exception_handlers=_exception_handlers,
    lifespan=[lifespan],
)

The benefit is not aesthetics. The benefit is that the runtime contract is inspectable. When a request enters the system, we can read the app construction and understand the order: request context binding, visit-specific CORS, demo-readonly protection, demo cache, demo rate limiting, global rate limiting, route dispatch, and exception normalization.

That difference also matters over the long term. FastAPI's ecosystem is enormous, but the framework itself has historically moved through a narrow maintainer path, with important pull requests depending heavily on review from the main author. For a small team, that creates a practical risk: when the framework does not ship an enterprise-oriented feature directly, we either write the plumbing ourselves or bring in another third-party utility. Litestar gave us more of those pieces in the framework, which meant less boilerplate, fewer glue layers, and a cleaner codebase for the parts of the product we need to keep changing quickly.

Dependencies belong to boundaries, not every handler

FastAPI's Depends() is approachable, but it can spread authorization and resource wiring across every endpoint signature. For our protected surface, Litestar's Provide model let us place authorization where the boundary exists: the router.

protected_router = Router(
    path="/",
    route_handlers=[
        *user.router,
        *competitors.router,
        *companies.router,
        *visibility.router,
        *ai_visibility.router,
        *causal.router,
    ],
    dependencies={"current_user": Provide(get_current_subscribed_user)},
)

That changed how we reason about the API. A handler inside protected_router is subscription-gated by construction. Payment routes can use a different router with a different provider. Public tracking routes stay outside both. The security model becomes structural instead of being repeated endpoint by endpoint.

Middleware is where product constraints show up

Our global CORS policy is intentionally strict because authenticated dashboard requests use credentials. The visit beacon is different: customer sites callPOST /visit from arbitrary origins, so that one path needs permissive CORS without weakening the rest of the API.

Litestar's middleware model made that straightforward. We wrote a small ASGI middleware that short-circuits OPTIONS /visit and injects permissive CORS headers only for /visit. The rest of the API still flows through the normal CORSConfig.

The same pattern is used for structlog request context. We clear and bind context variables at the start of every HTTP request, record status and latency at the end, and skip noisy health-check and static paths. It is simple ASGI code, and Litestar does not make it feel like a framework escape hatch.

Testing needed to stay async

Our tests use litestar.testing.AsyncTestClient, Beanie initialized against mongomock-motor, mocked Redis, mocked ClickHouse, and pytest-asyncio in auto mode. That matters because our app lifecycle opens async database clients, starts a scheduler, and runs handlers as coroutines.

@pytest_asyncio.fixture(scope="function")
async def client(test_db):
    async with AsyncTestClient(app=app) as test_client:
        yield test_client

The point is not that synchronous test clients are unusable. The point is that our highest-risk bugs tend to sit around async resource lifetime, request context, and patched service boundaries. Testing through an async client keeps the test harness closer to the production shape.

ASGI interop was not theoretical

The AI agent surface uses CopilotKit middleware that is FastAPI-native. We still wanted Litestar for the core API, so we mounted that FastAPI sub-app under /copilotkit through an ASGI route handler and adjusted the ASGI scope before passing control across.

@ASGIRouteHandler(path="/copilotkit", is_mount=True)
async def copilotkit_handler(scope, receive, send):
    stripped = scope["path"][len("/copilotkit"):] or "/"
    copilotkit_scope = {
        **scope,
        "path": stripped,
        "root_path": scope.get("root_path", "") + "/copilotkit",
    }
    await copilotkit_asgi(copilotkit_scope, receive, send)

This is the kind of integration detail that either stays boring or becomes a maintenance sink. In our case it stayed boring. Litestar gave us a clean ASGI boundary, and the CopilotKit app kept using the ecosystem it expected.

What we did not choose Litestar for

We did not base the migration on benchmark screenshots. Performance matters, but framework-level request benchmarks rarely predict the latency of a backend dominated by database calls, LLM calls, scraping providers, and analytics queries. We also did not choose Litestar because it would remove the need to understand ASGI, Pydantic, OpenAPI, or lifecycle management. It does not.

The gain was architectural fit. Litestar made the application graph more explicit, let us express dependencies at the level we actually enforce them, gave us direct ASGI-level control when product requirements needed it, and kept the tests aligned with an async production system.

The tradeoff

FastAPI has a larger mindshare and more examples on the internet. That has real value, especially for hiring and onboarding. The tradeoff is that we care less about the number of examples than about the shape of the code we have to own. Litestar asks the team to learn a smaller ecosystem and a few different idioms: handler lists instead of included routers everywhere, Provide instead of Depends, explicit middleware configuration, and DTOs or typed models when you want tighter control over request and response data.

For RISN AI, that tradeoff has been worth it. Our backend is a composition-heavy, async-first system with multiple authorization zones, multiple data stores, and a few nonstandard ASGI edges. Litestar matches that shape better than FastAPI did.

The RISN AI engineering team