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
operationIdnames the handler function. - The framework (
openapi_first) validates requests/responses against the spec and dispatches to the handler whose name equalsoperationId.
App bootstrap
lifespan calls the composition root:
Handler registry
interfaces/openapi.py is a flat re-export of every handler, so the framework
can resolve operationId across the whole app:
Handler shape
Handlers are thin and synchronous with respect to business logic:
Generated stubs. openapi-first routes emits this exact shape (async def
handlers taking body) when given --template-style; without that flag the
stub is def create_item(payload: ItemInput) — sync, with a payload
parameter. Both shapes bind identically: the router resolves handlers by
operationId and introspects the FastAPI signature, and it never inspects the
request body. Pass --template-style so the generated signatures match the
handlers you keep in interfaces/.
Conventions
operationIdnaming: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 globalsecurityblock exists. Leaving every operation without asecuritykey is the cleanest authless spec.
Profile endpoints (/users/me) and profile headers (x-profile) still exist in
an authless app. Define a stand-in identity in the composition root — for
example get_current_user returns or lazily creates a fixed "Demo User" — so
handlers stay auth-agnostic and only the wiring changes when real auth arrives.
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):
Prefer codegen over hand-writing boilerplate. The result is the wire contract
and a set of operationId-named signatures — not the canonical handlers.