4.3 KiB
Design — Guarantees, Startup Pipeline, and the Contract Model
This page is the architecture reference: the fail-fast startup surface, the guarantees that hold by construction, and the trade-offs baked into every decision.
🏗️ 1. The Startup Pipeline
Everything happens once, eagerly, at construction time — for the app at OpenAPIFirstApp(...), for the client at OpenAPIClient(...). There is no lazy loading, no deferred binding, no "it'll work on first request".
openapi.yaml ──► loader.load_openapi
├─ parse (json/yaml by extension)
├─ validate (strict OpenAPI 3.x validator)
└─► dict ──▶ OpenAPISpecLoadError
│
OpenAPIFirstApp(openapi_path=..., routes_module=routes)
├─ 1. load + validate spec (loader)
├─ 2. parse securitySchemes (security)
├─ 3. make_security_dependencies (security)
├─ 4. bind_routes: every operationId ──► routes.<operationId>
│ └─ missing op / missing handler ──► MissingOperationHandler
└─► FastAPI app: routes registry + /openapi.json + Swagger UI
Client — same spec, same guarantees:
OpenAPIClient(spec)
├─ require servers[]
├─ require paths
├─ one callable per operationId (client.<operationId>)
│ └─ missing / duplicate opId ──► OpenAPIClientError
└─► ready
📜 2. Guarantees That Hold by Construction
These are not conventions — they are enforced at startup or client construction:
- Every route is spec-declared. Routes are registered only from
paths; there is no decorator-driven or implicit routing. - Every operation is handled. Each
operationIdresolves to an exact handler name inroutes_module; a missing one aborts bootstrap. - Every operation has an
operationId. An operation without one cannot be bound and fails fast. - Client and server use the same spec. One document drives both sides, so they cannot drift.
- Auth is spec-driven.
securitydependencies come fromsecuritySchemes+ per-operationsecurity; no manual middleware. - Spec is valid before use. Invalid, malformed, or unloadable specs are rejected at load, not at first request.
The startup "no half-states" property
Because binding, validation, and security resolution all run at construction, a booted app is a provably-complete app. If the contract is violated in any way, the process refuses to start — the failure is loud, immediate, and tells you exactly what to fix.
🧩 3. Component Responsibilities
| Module | Owns | Refuses to |
|---|---|---|
loader |
parse + validate the spec | modify or "fix" the spec |
app |
assemble FastAPI from spec + handlers | routing decisions, decorators |
binder |
operationId → handler mapping |
infer handlers from paths |
client |
build one callable per operationId |
guess URLs, deserialize implicitly |
security |
schemes → FastAPI Depends |
manual middleware |
errors |
the error hierarchy | swallow failures |
codegen |
build-time model/routes generation | runtime codegen |
templates |
copyable scaffolds | production data stores |
Rule of thumb for contributors: keep modules coercive (they raise if the contract is wrong) and narrow (one responsibility each). Pydoclint + the test suite keep that contract honest.
⚖️ 4. Design Trade-offs (Accepted)
| Decision | Chosen because | What you give up |
|---|---|---|
| Handlers are plain callables | Framework-agnostic, testable, grep-able | No decorator sugar |
| Fail-fast at startup | Drift caught in CI, not at 3am | Slightly heavier boot |
Raw httpx.Response from client |
No hidden deserialization/validation | You read .json() yourself |
| Build-time codegen | One-way generation, no runtime generator dep | Spec must already be spec-valid |
| Templates copy verbatim | Scaffold, not magic | Templates never updated in place |