Add generate_routes(template_style=...) and CLI --template-style. When on, stubs use async def, a JSON body parameter named body (dict or Model), and with --inject-request a service-lookup hint comment before NotImplementedError. Default behaviour (def/payload) unchanged. +2 tests (32 passed), wiki + MCP bundle regenerated.
5.0 KiB
Codegen — Generate Models & Routes From Your Spec
TL;DR — point
openapi-first models/openapi-first routesat your spec and get Pydantic models plus operation-matching route stubs. The output is deterministic and is a starting point — not a maintained artifact.
🧭 1. Why Celebrate Codegen?
Two pain points kill OpenAPI projects:
- The "write the spec, then write the same thing as Pydantic models" step — exactly where server/client param types drift (your route says
item_id: int, your client sends a string…). - Typing handler names by hand when
bind_routesrequires eachoperationIdto resolve to a function of the exact same name.
Codegen collapses both. One command, one source file, the same shapes everywhere.
🚀 2. Model Generation
openapi-first models openapi.yaml -o app/_gen/models.py
The generated module mirrors the spec's components.schemas by name:
| Spec | Generated |
|---|---|
components.schemas.User |
class User(BaseModel) |
components.schemas.Item |
class Item(BaseModel) |
required + type from schema |
Pydantic Field(...) / type hints ✓ |
Never hand-edit those files. If the spec changes, regenerate — just like re-running a compiler after editing source.
Why constructor-time validation still applies
Codegen doesn't change the design: the generated models are plain Pydantic, and the client/server still validate against the spec at startup. Codegen is a convenience accelerator on top of the fail-fast guarantees in 04 – Design and 06 – Error Handling.
Wire model vs. domain model. Generated models are the wire contract. Keep them separate from your persisted/domain model (see the standards CRUD Template): give the wire schema an
id: str, but model the Mongo document so the id is optional.
🔁 3. Route Generation
openapi-first routes openapi.yaml -o app/interfaces \
--use-models --models-module app._gen.models \
--inject-request --package
One file per resource (derived from the first path segment). Flags:
| Flag | Effect |
|---|---|
--use-models |
Import request-body $ref models from --models-module |
--models-module MOD |
Dotted module for those imports (default models) |
--inject-request |
Prepend request: Request to every handler (DI convention) |
--package |
Emit <resource>/routes.py packages (with __init__.py) instead of flat <resource>.py |
--template-style |
Emit async def handlers with a JSON body parameter named body (the standards-template convention) plus a service-resolution hint, instead of sync def handlers taking payload |
Why bother? Because bind_routes (in 02 – Components) needs an operationId → handler exactly matching the spec. Codegen guarantees you never type a handler name wrong — it wears the same operationId the spec declares.
Resource names and extra media types
- A path such as
/fetch-requestsyields a sanitized module namefetch_requests.py(a raw hyphen could never be imported). multipart/form-dataandapplication/x-www-form-urlencodedbodies expand to one parameter per schema property:UploadFile = File(...)forformat: binary,Form(...)otherwise — not an opaquepayload: dict. Fields are required only when listed in the schema'srequired.
The stubs raise NotImplementedError; fill each body in (typically by resolving a service from request.app.state).
Handler shapes
Both shapes bind equally — the router resolves the handler by operationId and introspects the FastAPI signature, so it never inspects the request body:
- Default (sync stub):
def create_item(payload: ItemInput, response: Response):— the body parameter ispayload. --template-style:async def create_item(request: Request, body: ItemInput, response: Response):— matches the standards template (async def,bodyparameter, arequest.app.state.db_resources["<resource>_svc"]hint).
Use --template-style when generating against the standards template so the scaffold matches the hand-written handlers you will keep.
Note:
models/routesare build-time tooling. They do not run at runtime, and templates (the Bake-your-stuff section of 02 – Templates) scaffold a working server out-of-the-box for greenfield projects.
🧪 4. Idempotent Output
Generated output is deterministic w.r.t. the spec:
- Same spec → byte-identical files (unless you hand-edit — you won't)
- Ordering follows spec declaration order
- No timestamps, no machine names, no hidden randomness
That determinism is what lets you git diff after a spec change and see exactly what moved.