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.

Authless apps

Auth is deferred in the standard: the same spec-first app runs with or without it, because security lives entirely in the spec, never in handlers. For an app that needs no auth yet, do not invent per-handler auth checks — just omit the security keys:

  • Remove the top-level security: block.
  • Remove components.securitySchemes.
  • Do not add security: [] to operations. An explicit empty array is a per-operation override that means "public", and it only matters when a global security block exists. Leaving every operation without a security key is the cleanest authless spec.

The handlers, services, and composition root are unchanged. When auth is needed later, add the global security block and an introspecting dependency; only handlers that must opt out get security: [].

Codegen

openapi_first scaffolds models and route stubs from the spec (see the Build Recipe, step 3):

1
2
3
openapi-first models openapi.yaml -o _gen/models.py
openapi-first routes openapi.yaml -o _gen/routes \
  --use-models --models-module _gen.models --inject-request --package

Prefer codegen over hand-writing boilerplate. The result is the wire contract and a set of operationId-named signatures — not the canonical handlers.