Files
openapi-first/docs/wiki/01_overview.md

158 lines
5.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)