Files
openapi-first/docs/wiki/04_design.md

4.3 KiB
Raw Blame History

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:

  1. Every route is spec-declared. Routes are registered only from paths; there is no decorator-driven or implicit routing.
  2. Every operation is handled. Each operationId resolves to an exact handler name in routes_module; a missing one aborts bootstrap.
  3. Every operation has an operationId. An operation without one cannot be bound and fails fast.
  4. Client and server use the same spec. One document drives both sides, so they cannot drift.
  5. Auth is spec-driven. security dependencies come from securitySchemes + per-operation security; no manual middleware.
  6. 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