158 lines
5.6 KiB
Markdown
158 lines
5.6 KiB
Markdown
# 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**:
|
||
|
||
```yaml
|
||
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:
|
||
|
||
```python
|
||
# 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 `operationId` *must* resolve at startup
|
||
- 🔒 No auth can be bypassed — security is injected from `securitySchemes` + per-operation `security`, 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
|
||
|
||
```bash
|
||
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
|
||
|
||
```python
|
||
# 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):
|
||
|
||
```bash
|
||
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
|
||
|
||
```python
|
||
# 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`, `security` are the **library API surface** — stable, tested, documented.
|
||
- `templates` are **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 — `operationId` binding 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](03_use_cases/01_quickstart.md). Want to copy a runnable app? Jump to [02 – Templates](03_use_cases/02_templates.md). Digging into internals? See [Design](04_design.md).
|
||
|
||
---
|
||
|
||
## Related
|
||
|
||
- [01 – Quickstart](03_use_cases/01_quickstart.md) · [02 – Components](02_components.md) · [01 – Overview](01_overview.md)
|