Skip to content

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.

1
2
3
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 OpenAPISpecLoadError on: 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.

1
2
3
4
5
6
7
8
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):

  1. Load the spec (.yaml/.json).
  2. Validate it against OpenAPI 3.x schema.
  3. Parse securitySchemes and per-operation security.
  4. Build per-route security dependencies.
  5. Bind every path/method โ†’ handler by operationId; a missing handler, a missing operationId on 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:

  1. Reads the operation's operationId.
  2. Looks up routes_module.<operationId> โ€” a plain callable.
  3. Registers a FastAPI APIRoute bound to that handler, injecting Depends(...) 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:

1
2
3
4
5
6
7
8
9
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 servers entry (base_url falls 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; carries path, method, and optional operationId.

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]] โ€” builds METHOD:/path โ†’ dependency list from per-operation security, falling back to top-level security.

Supported scheme types (extensible):

  • type: http, scheme: bearer โ€” OpenAPIFirstSecurityDependency(HTTPBearer). With x-introspect-path/x-server-url, it introspects the JWT against an auth service; optionally sets request.state.user. Without introspection, it validates token presence and stores it on request.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