8.9 KiB
Components — What Ships in the Box
The library is deliberately small. Everything you need to run an OpenAPI-first service and talk to it with a strict client fits in a handful of modules.
🗂️ 1. Module Map
| Module | Responsibility | Import |
|---|---|---|
openapi_first.loader |
Load/validate the spec, resolve {ENV_VAR} |
load_openapi |
openapi_first.app |
OpenAPI-first FastAPI application bootstrap | OpenAPIFirstApp |
openapi_first.binder |
Spec → route binding via operationId |
bind_routes |
openapi_first.client |
operationId-driven HTTP client |
OpenAPIClient |
openapi_first.errors |
Explicit error hierarchy | OpenAPIFirstError, OpenAPIClientError, MissingOperationHandler |
openapi_first.security |
Security-dependency construction from the spec | parse_security_schemes, make_security_dependencies |
openapi_first.codegen |
Pydantic model generation (build-time) | generate_models |
openapi_first.codegen_routes |
Route-handler stub generation (build-time) | generate_routes |
openapi_first.cli |
scaffold / models / routes command surface |
CLI entry point |
openapi_first.templates |
Copyable application templates (NOT library API) | openapi-first scaffold |
⚙️ 2. loader — Load & Validate
load_openapi(path: str | Path) -> dict[str, Any] (in openapi_first/loader.py).
loader.py ensures a spec is real, readable, parseable, and schema-valid before anything else runs — a golden rule of fail-fast.
from openapi_first.loader import load_openapi
spec = load_openapi("openapi.yaml")
Behavior:
- Accepts
.json,.yaml,.yml(parsed by extension). - Runs strict OpenAPI 3.x validation (
openapi-spec-validator) at load time. - Raises
OpenAPISpecLoadErroron: missing file, unparseable content, or spec-validation failure. - Does not modify, coerce, or "fix" the spec — it's a read-only gate.
Env-var resolution lives at the security layer rather than the loader (see Security).
🧬 3. app — The Application Bootstrap
OpenAPIFirstApp (in openapi_first/app.py) is a FastAPI subclass that replaces manual route registration with OpenAPI-driven binding.
from openapi_first.app import OpenAPIFirstApp
import routes
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="My Service",
)
Startup pipeline (fail-fast, in this order):
- Load the spec (
.yaml/.json). - Validate it against OpenAPI 3.x schema.
- Parse
securitySchemesand per-operationsecurity. - Build per-route security dependencies.
- Bind every path/method → handler by
operationId; a missing handler, a missingoperationIdon a declared operation, or an unbound operation raises at startup.
Guarantees:
- Every route has a spec declaration (no undocumented routes).
- Every spec operation has a handler (no unhandled operations).
- Auth enforcement is spec-driven, not hand-wired.
/openapi.json+ Swagger UI always reflect the provided spec.
Keyword arguments beyond openapi_path / routes_module pass straight through to fastapi.FastAPI (it's a subclass — title, version, middleware, lifespan, … all work).
🔗 4. binder — Spec → Route Binding
bind_routes(app, spec, routes_module, security_deps=None) -> None (in openapi_first/binder.py).
This is the heart of the OpenAPI-first guarantee. For each path + HTTP method in the spec:
- Reads the operation's
operationId. - Looks up
routes_module.<operationId>— a plain callable. - Registers a FastAPI
APIRoutebound to that handler, injectingDepends(...)for any matching security requirements.
Failures are explicit and early:
| Condition | Raised |
|---|---|
No operationId on an operation |
MissingOperationHandler |
operationId has no handler function |
MissingOperationHandler |
| A path/method declared but unbound | MissingOperationHandler |
Handlers stay framework-agnostic: they're plain functions (payload, id, response) named after operationIds — no decorators, no routing metadata.
📡 5. client — The Other Side of the Contract
OpenAPIClient(spec, base_url=None, client=None) (in openapi_first/client.py).
The same spec that builds the server builds its client — one callable per operationId, keyed by name:
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
response = client.get_health()
response = client.get_user(path_params={"user_id": 1})
response = client.create_user(body={"name": "Ada"})
How operations become methods:
- Each
operationId→ a dynamically-built callable on the client. - Path parameters →
path_params={"user_id": 1}. - Request body →
body={...}(JSON) or raw content for non-JSON media types. - Query/headers →
query=/headers=. - Returns the raw
httpx.Response(no implicit deserialization, no hidden schema inference).
Client-construction guarantees (fail-fast, same philosophy as the app):
- Spec must declare at least one
serversentry (base_urlfalls back to it). - Spec must have
paths. - Every operation must have a unique
operationId. - Required params must match the spec at call time.
Construction errors raise OpenAPIClientError; request-time errors surface as httpx exceptions with operationId context.
💥 6. errors — Explicit Error Hierarchy
Everything raised by openapi-first derives from OpenAPIFirstError (in openapi_first/errors.py), so callers can handle first-party failures with a single except:
OpenAPIFirstError— base (comparable / aims to be picklable share).OpenAPIClientError— client-side contract violations.MissingOperationHandler— spec declares an operation whose handler is missing or unresolvable; carriespath,method, and optionaloperationId.
See Error Handling for the full table.
🛡️ 7. security — Auth from the Spec
security.py turns an OpenAPI securitySchemes section into FastAPI Depends(...) objects — no manual middleware.
parse_security_schemes(spec) -> dict[str, dict]— collects schemes, resolving{ENV_VAR}placeholders.make_security_dependencies(spec, security_schemes) -> dict[str, list[Depends]]— buildsMETHOD:/path→ dependency list from per-operationsecurity, falling back to top-levelsecurity.
Supported scheme types (extensible):
type: http, scheme: bearer—OpenAPIFirstSecurityDependency(HTTPBearer). Withx-introspect-path/x-server-url, it introspects the JWT against an auth service; optionally setsrequest.state.user. Without introspection, it validates token presence and stores it onrequest.state.token.- API-key style schemes via the same resolution path.
Key properties: resolution is per-operation, env placeholders resolve once at startup, and dependencies are injected by binder — your handlers never mention security.
🏭 8. codegen & cli — Build-Time Tooling
Generation is strictly build-time — spec → code, run once by a developer, committed:
openapi-first models <spec> -o <file>
Generates Pydantic models from spec schemas (wraps datamodel_code_generator):
# cli path
openapi-first models openapi.yaml -o app/models.py
openapi-first routes <spec> -o <dir> [--use-models] [--models-module models]
Generates one routes_<resource>.py stub file per resource, with NotImplementedError handlers bound to operationIds (optionally importing your generated models):
openapi-first routes openapi.yaml -o app/routes --use-models
openapi-first scaffold <template> [path]
Copies a bundled application template verbatim (see Templates).
🧱 9. templates — Copyable Applications
Four bundled, runnable applications live under openapi_first/templates/: health_app, crud_app, model_app, and vet_app. They are not part of the library API — no lint/format/type gates, never imported at runtime. They exist to be copied via:
openapi-first scaffold <template> [target-dir]
openapi-first scaffold --list