5.6 KiB
Overview — The OpenAPI-First Mental Model
openapi-first inverts the usual FastAPI workflow. Instead of decorating routes in code and letting FastAPI invent an OpenAPI document for you, you write one OpenAPI document first and let the library assemble both the application and the client from it. The spec is the contract; code is the implementation.
🧠 1. The Mental Model
1.1 One source of truth
Your OpenAPI document (openapi.yaml or openapi.json) is the single authoritative contract:
paths:
/health:
get:
operationId: get_health
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
Every route, method, parameter, schema, and security requirement lives here — and only here. Code never declares routes.
1.2 operationId is the binding key
The only bridge between the spec and your Python code is the operationId. Each operation maps, by name, to exactly one plain callable:
# routes.py
def get_health():
"""Health check operation handler."""
return {"status": "ok"}
openapi_first resolves operationId: get_health → routes.get_health and registers the route. No decorators, no @app.get, no routing metadata in code.
Guarantees this binding provides:
- 🚫 No undocumented route can exist — every route must be in the spec
- 🚫 No spec operation can go unhandled — every
operationIdmust resolve at startup - 🔒 No auth can be bypassed — security is injected from
securitySchemes+ per-operationsecurity, spec-driven - 🧱 No drift possible — the server and client are built from the same document
1.3 Fail-fast by design
Contract violations are detected at application startup (or client construction), never silently at request time:
| What goes wrong | When it fails |
|---|---|
| Invalid / unloadable spec | load_openapi at startup |
| Spec fails OpenAPI 3.x validation | load_openapi at startup |
operationId missing a handler |
bind_routes at startup |
Operation declared but no operationId |
bind_routes at startup |
Missing / duplicate operationId in client |
OpenAPIClient(...) construction |
⚙️ 2. What It Looks Like
2.1 Scaffold an application
openapi-first scaffold health_app my-health-service
openapi-first scaffold --list
scaffold copies a bundled template (verbatim — no code generation, no mutation) into a directory of your choice.
2.2 Bootstrap the server
# main.py
from openapi_first.app import OpenAPIFirstApp
import routes
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="My Service",
)
Run with your FastAPI-compatible server (ASGI):
uvicorn main:app --reload
FastAPI itself drives the server; every route, response model, and security dependency comes from the spec. /openapi.json and Swagger UI always reflect the spec, byte-for-byte.
2.3 Talk to it with the client
# client-side
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() # operationId-driven call
print(response.status_code) # 200
print(response.json()) # {"status": "ok"}
OpenAPIClient builds one callable per operationId from the same spec, so the client can never drift from the server.
🧩 3. Where Things Live
| Concern | Module |
|---|---|
| Load + validate spec | openapi_first.loader |
| Boot FastAPI app | openapi_first.app |
| Bind routes by opId | openapi_first.binder |
| HTTP client | openapi_first.client |
| Error hierarchy | openapi_first.errors |
| Security dependencies | openapi_first.security |
| Pydantic model codegen | openapi_first.codegen (uses datamodel_code_generator) |
| Route stub codegen | openapi_first.codegen_routes |
| Models / routes CLI | openapi_first.cli |
| Bundled templates | openapi_first.templates |
Jurisdictions:
loader,app,binder,client,errors,securityare the library API surface — stable, tested, documented.templatesare copyable scaffolds — not part of the library API; excluded from lint/format/type gates, never imported at runtime.
📄 4. The Non-Goals
openapi-first deliberately does not:
- Parse decorators to generate an OpenAPI schema (that's default FastAPI behavior — the inverse)
- Generate models from code at runtime (only at build time via CLI, from spec → Pydantic)
- Validate request/response bodies against the spec at runtime (contract is enforced at startup / client construction; FastAPI + Pydantic handle runtime coercion)
- Invent routing from path conventions —
operationIdbinding only - Ship a production feature set in the bundled templates (they're demos/scaffolds: in-memory stores, no concurrency, no auth configured)
🧭 5. Path Forward
New here? Start with 01 – Quickstart. Want to copy a runnable app? Jump to 02 – Templates. Digging into internals? See Design.