docs: add OpenAPI-first wiki (overview, components, use cases for templates/client/codegen, design, security, error handling, testing) and refresh lib docs index

This commit is contained in:
2026-09-15 22:29:05 +05:30
parent adeb02e162
commit b559323dfe
26 changed files with 1590 additions and 86 deletions

View File

@@ -0,0 +1,122 @@
# Use Case 1: Quickstart — Build Your First OpenAPI-First Service
This guide walks you from an empty directory to a running, contract-driven service in a few minutes, then talks to it with the generated client.
---
## 🛠️ 1. Prerequisites
- Python 3.10+
- `openapi-first` installed (see [Overview](../01_overview.md#installation))
- `pip install "fastapi[standard]"` (or `uvicorn`) to run the app
---
## 📄 2. Write the OpenAPI document
OpenAPI comes first. Create `openapi.yaml`:
```yaml
openapi: 3.0.3
info:
title: Greeting Service
version: 1.0.0
servers:
- url: http://localhost:8000
paths:
/greet/{name}:
get:
operationId: get_greeting
parameters:
- name: name
in: path
required: true
schema:
type: string
responses:
"200":
description: A greeting
content:
application/json:
schema:
type: object
properties:
greeting:
type: string
```
Key points: every operation needs `operationId`, and every route must exist *only* here.
---
## 🧑‍💻 3. Write the handlers
Create `routes.py` — plain functions, no decorators, named exactly like the `operationId`s:
```python
# routes.py
def get_greeting(name: str) -> dict:
"""Return a greeting for the given name."""
return {"greeting": f"Hello, {name}!"}
```
If a handler is missing at startup, the app **refuses to boot** (`MissingOperationHandler`) — the fail-fast guarantee catches contract drift immediately.
---
## 🚀 4. Bootstrap the app
Create `main.py`:
```python
# main.py
from openapi_first.app import OpenAPIFirstApp
import routes
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="Greeting Service",
)
```
Run it:
```bash
uvicorn main:app --reload
```
Visit `http://localhost:8000/docs` (Swagger UI) and `http://localhost:8000/openapi.json` — both are generated from *your* spec.
---
## 📡 5. Call it with the client
The same spec builds a strict client:
```python
# client.py
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
response = client.get_greeting(path_params={"name": "Ada"})
print(response.status_code) # 200
print(response.json()) # {"greeting": "Hello, Ada!"}
```
---
## 💡 6. Next Steps
- Copy a fuller example: [02 Templates](02_templates.md)
- Generate models/routes from a bigger spec: [04 Codegen](04_codegen.md)
- Drive everything from a client: [03 Client](03_client.md)
---
## Related
- [01 Overview](../01_overview.md) · [02 Components](../02_components.md) · [01 Quickstart](01_quickstart.md)

View File

@@ -0,0 +1,167 @@
# Use Case 2: Templates — Copyable Reference Applications
`openapi-first` ships four runnable, copyable applications under `openapi_first/templates/`. They are **not part of the library API** — they are bundled scaffold examples you copy into your own project and build on.
---
## 🎬 1. What Templates Are
A template is a complete, self-contained OpenAPI-first service:
- A bundled directory inside `openapi_first/templates/<name>/`
- Copyable **verbatim** — no code generation, no mutation — via the CLI
- Each one demonstrates a specific set of OpenAPI-first behaviors and FastAPI features
All templates share the same skeleton:
```
<name>_app/
├── __init__.py # explains the template + how to scaffold it
├── openapi.yaml # the contract (source of truth)
├── main.py # assembles OpenAPIFirstApp from the spec
├── routes.py # operationId-bound handler functions
└── data.py # in-memory data store (demo only)
```
---
## 📋 2. The Four Templates
### 2.1 `health_app` — minimal liveness probe
```bash
openapi-first scaffold health_app
# or into a custom directory:
openapi-first scaffold health_app my-health-service
```
| File | Purpose |
|------|---------|
| `openapi.yaml` | `GET /health``operationId: get_health` |
| `routes.py` | `get_health()` returns `{"status": "ok"}` |
| `main.py` | `OpenAPIFirstApp(openapi_path="openapi.yaml", routes_module=routes)` |
**Why it exists:** the absolute minimal OpenAPI-first round trip — one operation, one handler, zero moving parts. The best starting point to internalize the mental model.
**Smoke test:**
```bash
pip install -e .
uvicorn main:app
curl http://localhost:8000/health
# → {"status": "ok"}
```
### 2.2 `crud_app` — dict-based CRUD
```bash
openapi-first scaffold crud_app my-crud-service
```
| File | Purpose |
|------|---------|
| `openapi.yaml` | Full CRUD over `/items` (list/get/create/update/delete) |
| `routes.py` | Handlers bound via `operationId`s `list_items`, `get_item`, `create_item`, `update_item`, `delete_item` |
| `data.py` | In-memory dict store with auto-incrementing `id` |
Behaviors you learn:
- **Explicit status codes** — `create_item`/`delete_item` take `response: Response` and set `201`/`204`; `get_item`/`update_item` raise `HTTPException(404)` on `KeyError`
- **Handlers as plain callables** — no FastAPI decorators, routing comes solely from the spec
- **Mock data store** — `data.py` is a copyable in-memory store, explicitly **not** production-ready
### 2.3 `model_app` — Pydantic model CRUD
```bash
openapi-first scaffold model_app my-model-service
```
| File | Purpose |
|------|---------|
| `openapi.yaml` | Same CRUD surface, schemas reference models |
| `models.py` | Pydantic `Item`, `ItemCreate`, `ItemBase` (request/response models) |
| `routes.py` | Handlers type-annotated with the models; `create_item` sets `201` |
| `data.py` | In-memory store returning real model instances |
Behaviors you learn:
- **Pydantic request/response models** — payloads validated and serialized via FastAPI
- **Same handler contracts** — identical `operationId` set as `crud_app`, so the two are interchangeable
- **Models in the client too** — the same spec drives `OpenAPIClient` body handling
### 2.4 `vet_app` — the full-featured demo
```bash
openapi-first scaffold vet_app my-vet-clinic
```
| File | Purpose |
|------|---------|
| `openapi.yaml` | Five resources (parents, vets, treatments, pets, appointments) + SSE + upload + discriminated unions |
| `models.py` | Pydantic models incl. **discriminated unions** (`noteType` literal fields) |
| `routes.py` | ~20 handlers across all resources, incl. pagination, filtering, photo upload, SSE streaming |
| `sse.py` | Server-Sent Events helper (`StreamingResponse`, per-pet subscriber queues, background `asyncio` workers) |
| `data.py` | Larger in-memory store (parents → vets → treatments → pets → appointments) |
| `main.py` | App + CORS + lifespan example |
Behaviors you learn — the *advanced* tier:
- **Discriminated unions** — `ProcedureNotes` uses `oneOf` + `discriminator.noteType` mapping; Pydantic models use `Literal[...]` discriminator fields
- **SSE streaming** — a `GET /pets/{id}/actions` operation streaming `text/event-stream` via `StreamingResponse` with background task workers
- **File upload** — `UploadFile` handler setting a multi-part body
- **CORS + middleware** — `add_middleware(CORSMiddleware, ...)` alongside the spec-driven setup
- **Response injection** — handlers set `201`/`204` explicitly via injected `Response`
---
## 🚀 3. CLI Reference
```bash
# List available templates
openapi-first scaffold --list
# Copy a template into its default directory (template name, dashes)
openapi-first scaffold health_app
# Copy into a custom target directory
openapi-first scaffold crud_app my-project/crud
```
> **Protip:** `DEFAULT_TEMPLATE` is `health_app`, so `openapi-first scaffold` with no template name scaffolds the health app.
---
## 🧩 4. Anatomy of a Scaffolded Service
After `openapi-first scaffold health_app my-health-service`, your directory contains a drop-in FastAPI service:
```
my-health-service/
├── openapi.yaml # THE contract
├── main.py # `app = OpenAPIFirstApp(openapi_path=..., routes_module=routes)`
└── routes.py # `def get_health(): ...`
```
Run it:
```bash
cd my-health-service
pip install -e .
uvicorn main:app --reload
```
`/docs`, `/openapi.json`, and every declared route now exist — all derived from `openapi.yaml`.
---
## ⚠️ 5. Production Disclaimer
Templates use **in-memory, non-persistent, non-concurrency-safe** data stores. They are learning scaffolds — **not** production references. Swap in a real data layer (SQL/REDIS/object store) the moment you go beyond a demo.
See the `__init__.py` of each template for detailed client examples, CLI examples, and design notes.
---
## Related
- [01 Quickstart](01_quickstart.md) · [04 Codegen](04_codegen.md) · [02 Components](../02_components.md)

View File

@@ -0,0 +1,156 @@
# Use Case 3: The OperationId-Driven Client
`OpenAPIClient` is the other side of the contract. It reads the *same* OpenAPI document the server runs on and exposes one callable per `operationId` — so "client" and "server" are two views of one truth.
---
## 🔍 1. Before You Start
The client is `httpx`-based and returns raw `httpx.Response` objects — no magic deserialization, no hidden schema inference:
- **No** response Pydantic models
- **No** implicit URL construction (path params are explicit)
- **No** hand-written `requests.get(...)` scattered through your code
---
## 🧬 2. Constructing the Client
```python
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
```
The base URL comes from the spec's `servers` list (first entry) unless you pass `base_url` explicitly:
```python
client = OpenAPIClient(spec, base_url="https://api.internal.myco/v1")
```
You can also hand over a preconfigured `httpx.Client` (custom transport, TLS, retries):
```python
import httpx
transport = httpx.HTTPTransport(retries=3)
client = OpenAPIClient(
spec,
client=httpx.Client(transport=transport),
)
```
---
## 💥 3. Fail-Fast at Construction
`OpenAPIClient(...)` **raises immediately** if the contract is broken — you find out at startup, not on the first request:
| Violation | Error |
|----------------------------------------|-----------------------------------|
| Spec has no `servers` entry | `OpenAPIClientError` |
| Spec has no `paths` | `OpenAPIClientError` |
| Operation missing `operationId` | `OpenAPIClientError` |
| Duplicate `operationId` | `OpenAPIClientError` |
| Operation references unknown parameters| `OpenAPIClientError` |
Missing *required parameters* fail at **call time** — pydoclint-grade strictness on the wire.
---
## 📞 4. Calling Operations
Every `operationId` becomes a method. The call signature is uniform across the whole client:
```python
response = client.<operationId>(
*,
path_params: dict | None = None,
query: dict | None = None,
headers: dict | None = None,
body: Any | None = None,
timeout: float | None = None,
) -> httpx.Response
```
Concrete examples (from the `crud_app` / `health_app` templates):
```python
# No parameters — simplest
response = client.get_health()
assert response.status_code == 200
# Path parameter
response = client.get_item(path_params={"item_id": 3})
# Query parameters
response = client.list_items(query={"limit": 10, "offset": 20})
# JSON request body
response = client.create_item(body={"name": "Orange", "price": 0.8})
# Custom headers / timeout
response = client.get_user(
path_params={"user_id": 1},
headers={"X-Internal-Key": "..."},
timeout=30,
)
```
**Returns:** the raw `httpx.Response`, so `status_code`, `.json()`, `.headers` are all yours to inspect.
---
## 🧠 5. How Parameters Are Bound
For each operation the client knows exactly *where* each parameter belongs:
| OpenAPI location | Client kwarg |
|------------------|-------------------|
| `in: path` | `path_params[name]` |
| `in: query` | `query[name]` |
| `in: header` | `headers[name]` |
| `requestBody` | `body` |
JSON media types are sent as `json=`; other media types as raw `content=`.
---
## 🔄 6. Server ↔ Client, One Spec
```bash
# One directory, two processes
uvicorn main:app --port 8000 # server
python -c "import asyncio; from client_script import run; asyncio.run(run())" # client
```
Or the same client against a remote environment:
```python
client = OpenAPIClient(
spec,
base_url="https://staging.internal.myco",
)
```
The URL is the *only* thing that changes between environments — the contract never does.
---
## ✍️ 7. OperationId as the API
Because the client is operationId-driven:
- Adding an operation = adding a method (and vice versa)
- No operation can be "forgotten" by the client — it's constructed from the spec
- `operationId` is the one name you remember; the HTTP verb/path is an implementation detail
If an `operationId` you call doesn't exist, you get an `AttributeError` at construction-scan time — never a silent 404.
---
## Related
- [01 Overview](../01_overview.md) · [01 Quickstart](01_quickstart.md) · [02 Templates](02_templates.md)

View File

@@ -0,0 +1,70 @@
# Codegen — Generate Models & Routes From Your Spec
> **TL;DR** — point `openapi-first codegen` at your spec and get `models` + `routes` scaffolds you can bind with one line. The codegen output is deterministic, and it's a *starting point* — not a maintained artifact.
---
## 🧭 1. Why Celebrate Codegen?
Two pain points kill OpenAPI projects:
1. **The "write the spec, then write the same thing as Pydantic models" step** — exactly where server/client param types drift (your route says `item_id: int`, your client sends a string…).
2. **Writing the docs/spec catalog by hand**, once you have more than a handful of operations.
Codegen collapses both. One command, one source file, same generated shapes everywhere.
---
## 🚀 2. Model Generation
```bash
openapi-first codegen models --module my_project.models --input openapi.yaml
```
The generated model module mirrors the spec's `components.schemas` **by name**:
| Spec | Generated |
|------|-----------|
| `components.schemas.User` | `class User(BaseModel)` |
| `components.schemas.Item` | `class Item(BaseModel)` |
| every `$ref` (schema) | a `type: ClassVar` alias ✓ |
| `required + type` from schema | Pydantic `Field(...)` / type hints ✓ |
**Never hand-edit those files.** If the spec changes, regenerate — just like you'd re-run `cargo build` after editing `Cargo.toml`.
### Why constructor-time validation still applies
Codegen doesn't change the design: the generated models are plain Pydantic, and the **client/server still validate against the spec** at startup. Codegen is a *convenience accelerator* on top of the fail-fast guarantees in [04 Design](../04_design.md) and [06 Error Handling](../06_error_handling.md).
---
## 🔁 3. Route Generation / Verification
```bash
# dry-run verification against your routes module
openapi-first codegen routes --module my_routes --input openapi.yaml --check
# scaffold an operation skeleton (generates the handler with a TODO)
openapi-first codegen routes --module my_routes --input openapi.yaml
```
Why bother? Because `bind_routes` (in [02 Components](../02_components.md)) needs an `operationId` → handler **exactly** matching the spec. Codegen guarantees you never type a handler name wrong — it wears the same `operationId` as the spec says.
> **Note:** `codegen` is build-time tooling. It does **not** run at runtime, and templates (the Bake-your-stuff section of [02 Templates](02_templates.md)) make scaffolding a server out-of-the-box even simpler for greenfield projects.
---
## 🧪 4. Idempotent Output
Generated output is deterministic w.r.t. the spec:
- Same spec → byte-identical files (unless you hand-edit — you won't)
- Ordering follows spec declaration order
- No timestamps, no machine names, no hidden randomness
That determinism is what lets you `git diff` after a spec change and see **exactly** what moved.
---
## 🧩 Related
- [01 Overview](../01_overview.md) · [02 Templates](02_templates.md) · [03 Client](03_client.md) · [04 Design](../04_design.md) · [07 Testing](../07_testing.md)