Skip to content

Coming from FastAPI

If you already write FastAPI, you already know most of the HTTP layer QuadKit uses. Starlette routing. Pydantic request shapes. OpenAPI. TestClient-shaped tests. That is not an accident — both sit on Starlette.

This is not a replacement pitch. It is a map of what stays in your hands, and what moves one level up so the rest of the application — services, data access, background work — feels as designed as the routes.

A path operation still looks like a path operation:

from quadkit.web import Controller, get
class UserController(Controller):
prefix = "/users"
@get("/{user_id}")
async def get_user(self, user_id: str) -> dict:
return {"id": user_id, "name": "Ada"}

{user_id} is the same Starlette syntax. Pydantic models still work for bodies. OpenAPI still falls out of the web layer. If you have been writing FastAPI for years, this page should feel like a dialect, not a new language.

FastAPI’s Depends() is a good idea: declare what the handler needs, get it resolved. QuadKit does the same work on the constructor, so a service is equally easy to call from a route, a task, or a test.

class OrderController(Controller):
prefix = "/orders"
def __init__(self, repo: OrderRepository) -> None:
self.repo = repo
@get("/")
async def list_orders(self) -> list[dict]:
return await self.repo.find_all()

Startup and shutdown hooks still exist as a pattern. QuadKit names them register(), boot(), and shutdown(), ordered by ProviderPriority, so the database is connected before the first request and torn down in reverse.

Expected HTTP failures still become status codes. In the domain they are Result values, so the same UserService.find() works behind a queue or a CLI without raising HTTPException into the wrong layer.

The reason to add a composition root is not the first route. It is the fifth backend.

FastAPI does not stop you from importing SQLAlchemy in a handler. That is a reasonable way to start. QuadKit’s bet is that when SQL, cache, and an LLM client all show up, they should talk through quadkit-contracts — so swapping Redis for in-memory in tests, or Postgres for a replica, is config, not a rewrite of callers.

You install what you need. quadkit-web is the HTTP layer; quadkit-cli and quadkit-testing are tooling. They never import each other — the boundary is a law, not a convention.

A single new service, or a single new endpoint, is a valid first step. Keep the FastAPI app you already have. Port one bounded context when you want the container, the providers, and the contracts in the same place as the routes.

The migration guide is the concept map: Depends() → constructor, TestClient → WebTestBed, app.state → the container, HTTPException → Result at the domain and HTTP at the edge.

If Laravel’s DX is how the rest of the stack is shaped, that story is here. If you just want a compiling app:

Terminal window
uv add "quadkit-cli>=0.0.4,<0.1"
quadkit new project my-app --template web-api
quadkit run

Walkthrough: Your First App.