Skip to content

Spec-First Routing

Summary

The OpenAPI document is the single source of truth for the HTTP contract. Handlers are bound to it by name; there is no decorator router.


The contract

  • openapi.yaml (or .json) declares paths, schemas, parameters, responses, and security.
  • A custom operationId names the handler function.
  • The framework (openapi_first) validates requests/responses against the spec and dispatches to the handler whose name equals operationId.

App bootstrap

# main.py
from openapi_first.app import OpenAPIFirstApp

app = OpenAPIFirstApp(
    openapi_path="openapi.yaml",
    routes_module=interfaces.openapi,   # module whose namespace holds handlers
    title="<App> API",
    version="0.1.0",
    lifespan=lifespan,
)

lifespan calls the composition root:

1
2
3
4
5
@asynccontextmanager
async def lifespan(app):
    app.state.db_resources = await init()
    yield
    await shutdown()

Handler registry

interfaces/openapi.py is a flat re-export of every handler, so the framework can resolve operationId across the whole app:

1
2
3
from interfaces.item.routes import *
from interfaces.wallet.routes import *
from interfaces.progress.sse import *

Handler shape

Handlers are thin and synchronous with respect to business logic:

1
2
3
async def create_item(request: Request, body: dict):
    svc = request.app.state.db_resources["item_svc"]
    return await svc.create(Item.model_validate(body))

Conventions

  • operationId naming: list_<resource>, create_<resource>, get_<resource>, update_<resource>, delete_<resource>.
  • Read-only endpoints may use a cache decorator keyed on path + query + user.
  • Validation errors -> 400; not found -> 404; delete -> 204.
  • Security is declared once in the spec and enforced by the framework; handlers contain no auth code.

Codegen

The openapi_first library can scaffold models and route stubs from the spec, and ships app templates (CRUD app, model app, service). Prefer codegen over hand-writing boilerplate.