{ "module": "spec_first_routing", "content": { "path": "spec_first_routing", "docstring": "# Spec-First Routing\n\n## Summary\n\nThe OpenAPI document is the single source of truth for the HTTP contract.\nHandlers are bound to it by name; there is no decorator router.\n\n---\n\n## The contract\n\n- `openapi.yaml` (or `.json`) declares paths, schemas, parameters, responses,\n and security.\n- A custom `operationId` names the handler function.\n- The framework (`openapi_first`) validates requests/responses against the\n spec and dispatches to the handler whose name equals `operationId`.\n\n## App bootstrap\n\n```python\n# main.py\nfrom openapi_first.app import OpenAPIFirstApp\n\napp = OpenAPIFirstApp(\n openapi_path=\"openapi.yaml\",\n routes_module=interfaces.openapi, # module whose namespace holds handlers\n title=\" API\",\n version=\"0.1.0\",\n lifespan=lifespan,\n)\n```\n\n`lifespan` calls the composition root:\n\n```python\n@asynccontextmanager\nasync def lifespan(app):\n app.state.db_resources = await init()\n yield\n await shutdown()\n```\n\n## Handler registry\n\n`interfaces/openapi.py` is a flat re-export of every handler, so the framework\ncan resolve `operationId` across the whole app:\n\n```python\nfrom interfaces.item.routes import *\nfrom interfaces.wallet.routes import *\nfrom interfaces.progress.sse import *\n```\n\n## Handler shape\n\nHandlers are thin and synchronous with respect to business logic:\n\n```python\nasync def create_item(request: Request, body: dict):\n svc = request.app.state.db_resources[\"item_svc\"]\n return await svc.create(Item.model_validate(body))\n```\n\n## Conventions\n\n- `operationId` naming: `list_`, `create_`,\n `get_`, `update_`, `delete_`.\n- Read-only endpoints may use a cache decorator keyed on path + query + user.\n- Validation errors -> `400`; not found -> `404`; delete -> `204`.\n- Security is declared once in the spec and enforced by the framework; handlers\n contain no auth code.\n\n## Authless apps\n\nAuth is **deferred** in the standard: the same spec-first app runs with or\nwithout it, because security lives entirely in the spec, never in handlers.\nFor an app that needs no auth yet, do **not** invent per-handler auth checks —\njust omit the security keys:\n\n- Remove the top-level `security:` block.\n- Remove `components.securitySchemes`.\n- Do **not** add `security: []` to operations. An explicit empty array is a\n per-operation *override* that means \"public\", and it only matters when a\n global `security` block exists. Leaving every operation without a `security`\n key is the cleanest authless spec.\n\nThe handlers, services, and composition root are unchanged. When auth is needed\nlater, add the global `security` block and an introspecting dependency; only\nhandlers that must opt out get `security: []`.\n\n## Codegen\n\n`openapi_first` scaffolds models and route stubs from the spec (see the\n[Build Recipe](01_build_recipe.md), step 3):\n\n```bash\nopenapi-first models openapi.yaml -o _gen/models.py\nopenapi-first routes openapi.yaml -o _gen/routes \\\n --use-models --models-module _gen.models --inject-request --package\n```\n\nPrefer codegen over hand-writing boilerplate. The result is the wire contract\nand a set of `operationId`-named signatures — not the canonical handlers.\n", "objects": {} } }