Files
mizan/backends/mizan-fastapi/README.md
Ryth Azhur e00b3a177e Docs carry the shape of the system, not its status
MIZAN.md's phase-numbered implementation order, PRODUCT_ARCHITECTURE's
"Deferred until Render revenue funds it" and "Shipped in", PSR_VS_EDGE's
current-state section, and the READMEs' passing-test counts were all reporting
where the work stood rather than what the system is. OWED_SURFACE keeps its
subject — surface that is specified but unbuilt — stated as the shape each unit
owes.

The channel sections follow the renamed slots: Params / ClientMessage /
ServerMessage, and Channel as the base class on both backends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:03:29 -04:00

233 lines
6.8 KiB
Markdown

# mizan-fastapi
FastAPI backend adapter for the Mizan protocol. One decorator on a server
function. Typed React client generated. Invalidation automatic.
## Scope
mizan-fastapi's surface is RPC dispatch, context bundling, JSON-body
invalidation, auth gating, and channels over a multiplexed WebSocket. Forms,
Shapes, and SSR sit outside that surface — a FastAPI project reaches for its
own native equivalents (Pydantic, ORM-of-choice, FastAPI's SSR ecosystem).
## Install
```bash
uv add mizan-fastapi
```
## Setup
```python
# main.py
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from mizan_fastapi import (
MizanError,
mizan_exception_handler,
mizan_validation_handler,
router as mizan_router,
ws_router,
)
app = FastAPI()
app.include_router(mizan_router, prefix="/api/mizan")
app.include_router(ws_router, prefix="/api/mizan")
app.add_exception_handler(MizanError, mizan_exception_handler)
app.add_exception_handler(RequestValidationError, mizan_validation_handler)
```
The exception handlers render every error path through the Mizan envelope
(`{"error": {"code", "message", "details"}}`) so the kernel's `MizanError`
parses status + code on the frontend regardless of which failure happened.
## Define server functions
```python
from mizan_core.client.function import client
from mizan_core.registry import register
from pydantic import BaseModel
class EchoOutput(BaseModel):
message: str
@client
def echo(request, text: str) -> EchoOutput:
return EchoOutput(message=text)
register(echo, "echo")
```
mizan-fastapi has no auto-discovery (FastAPI doesn't have an app registry
to walk). Register every `@client`-decorated function explicitly. A typical
project keeps registrations in `main.py` (alongside the FastAPI app) or in
a dedicated `clients.py` imported during startup.
## `@client` parameters
```python
@client # plain RPC function
@client(context="global") # singleton context — fetched once, SSR-hydrated
@client(context="user") # named context — fetched per provider mount
@client(affects="user") # mutation — invalidates the user context
@client(affects=user_profile) # mutation — invalidates a specific function
@client(auth=True) # requires authentication
@client(auth="staff") # requires is_staff
@client(auth="superuser") # requires is_superuser
@client(auth=lambda req: ...) # custom predicate
@client(rev=2) # cache revision (busts on bump)
```
Forms parameters are accepted by the decorator (they're a `mizan-core`
primitive) and carry no meaning to this adapter.
## Channels
A channel is a named fan-out over the WebSocket `ws_router` serves. Subclass
`Channel`, declare whichever payload models the channel carries, and register
it. The three model names are read from the client's side: `Params` keys the
fan-out, `ClientMessage` travels up, `ServerMessage` travels down.
```python
from mizan_fastapi import Channel, register_channel
from pydantic import BaseModel
class Chat(Channel):
class Params(BaseModel):
room: str
class ClientMessage(BaseModel):
text: str
class ServerMessage(BaseModel):
user: str
text: str
def authorize(self, params: Params) -> bool:
return True
def receive(self, params: Params, msg: ClientMessage) -> ServerMessage:
return self.ServerMessage(user="anon", text=msg.text)
register_channel(Chat, "chat")
```
Server code pushes to a group from anywhere:
```python
await Chat.push(Chat.ServerMessage(user="system", text="hello"), room="general")
```
Registered channels contribute to the exported IR, so codegen emits the
`<Pascal>Params` / `<Pascal>ClientMessage` / `<Pascal>ServerMessage` types and
the matching frontend hook.
Group membership lives in the process that holds the socket, so a push reaches
only the subscribers attached to that process. Fan-out that spans processes is
a shared broker in front of `broadcast`.
## Auth integration
The executor expects `request.state.user` to be populated by your FastAPI
middleware or dependency tree before dispatch:
```python
from fastapi import Request
@app.middleware("http")
async def attach_user(request: Request, call_next):
request.state.user = await resolve_user_from_token(request)
return await call_next(request)
```
Where `resolve_user_from_token` returns either a user object with
`is_authenticated`, `is_staff`, `is_superuser` attributes, or `None` for an
anonymous request. The executor branches on those for `auth=True`,
`auth="staff"`, `auth="superuser"` requirements.
## Generate the frontend
The codegen is the `mizan-generate` Rust binary (source at
`protocol/mizan-codegen/`; `protocol/mizan-generate/` is a thin npm
launcher that dispatches to the platform binary). Point a `mizan.toml` at
your FastAPI app and run the CLI:
```toml
# frontend/mizan.toml
output = "src/api"
targets = ["react"]
[source.fastapi]
module = "main" # module to import for @client side effects
cwd = "../backend" # python cwd for module resolution
command = ["uv", "run", "python"] # optional — defaults to ["python"]
```
```bash
mizan-generate --config mizan.toml
```
The codegen drives `python -m mizan_fastapi.ir <module>` under the hood,
parses the emitted KDL IR, then emits Stage 1 (typed `callXxx`/`fetchXxx`
over the runtime kernel) + Stage 2 (`<MizanContext>` provider, per-context
providers, `use{Hook}()` hooks) into `src/api/`.
```tsx
// app.tsx
import { MizanContext } from "./api"
export default function App({ children }) {
return <MizanContext baseUrl="/api/mizan">{children}</MizanContext>
}
```
```tsx
// any component
import { useEcho, useCurrentUser } from "./api"
const echo = useEcho()
echo.mutate({ text: "hi" }).then(r => console.log(r.message))
const user = useCurrentUser() // global context — auto-fetched, auto-refreshed on mutation
```
## Running tests
```bash
uv sync --extra dev
uv run pytest
```
## Schema export CLI
For codegen consumption (or any tooling that wants the Mizan schema):
```bash
python -m mizan_fastapi.ir <module>
```
Imports the named module (which must register every `@client` function as
import-time side effects), then prints the Mizan KDL IR to stdout.
## Architecture
mizan-fastapi is one of two reference backend adapters (the other is
`backends/mizan-django`). Both implement the same Mizan protocol on top of
the shared `cores/mizan-python` core (`@client`, registry, MWT, HMAC cache
keys). The AFI conformance suite at `tests/afi/` gates that the two adapters
emit equivalent schemas for the same registered functions. See
`docs/AFI_ARCHITECTURE.md`.
A live e2e harness exercises this adapter end-to-end at
`examples/fastapi-react-site/` (real Chromium → React with generated hooks
→ FastAPI server, driven by Playwright).