Renamed: DjangoError → MizanError DjangoHTTPClient → MizanHTTPClient DjangoFormState → MizanFormState DjangoFormsetState → MizanFormsetState createDjangoCSRClient → createMizanCSRClient createDjangoSSRClient → createMizanSSRClient ensureDjangoSession → ensureMizanSession useDjangoCSRClient → useMizanCSRClient TDjangoMessage → TServerMessage Made CSRF configurable: configureCsrf(cookieName, headerName) — defaults to Django conventions but works with any backend that uses CSRF tokens. Cookie name and header name are no longer hardcoded. All old names preserved as deprecated aliases in index.ts exports for backwards compatibility. Removed dead RouterAdapter re-export (file moved to legacy/). 33 React tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mizan
Django + React server functions framework. RPC, not REST.
You define Python functions. mizan generates typed React hooks. No API routes, no serializers, no endpoint boilerplate.
# Django
@client(context='global')
def current_user(request) -> UserOutput:
return UserOutput(email=request.user.email)
// React (generated)
const user = useCurrentUser() // typed, SSR-hydrated, auto-refreshed
Packages
| Package | Path | Install |
|---|---|---|
mizan (Python) |
django/ |
uv add "mizan[channels] @ git+..." |
@rythazhur/mizan (TypeScript) |
react/ |
npm install @rythazhur/mizan@git+... |
Quick Start
1. Django setup
# settings.py
INSTALLED_APPS = [
"mizan",
"myapp",
]
# urls.py
from django.urls import include, path
urlpatterns = [
path("api/mizan/", include("mizan.urls")),
]
# asgi.py (for WebSocket support)
from mizan import wrap_asgi
from django.core.asgi import get_asgi_application
application = wrap_asgi(get_asgi_application())
2. Define server functions
# myapp/mizan_clients.py
from django.http import HttpRequest
from mizan.client import client
from mizan.setup.registry import register
from pydantic import BaseModel
class EchoOutput(BaseModel):
message: str
@client
def echo(request: HttpRequest, text: str) -> EchoOutput:
return EchoOutput(message=text)
register(echo, "echo")
3. Register in apps.py
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
import myapp.mizan_clients # noqa: F401
4. Generate TypeScript
# django.config.mjs
export default {
source: {
django: {
managePath: '../backend/manage.py',
command: ['uv', 'run', 'python'],
},
},
output: 'src/api/generated.ts',
}
npx mizan-generate
This produces typed hooks, a typed provider, form hooks with Zod validation, and channel hooks.
5. Use in React
// layout.tsx
import { DjangoContext } from '@/api'
export default function Layout({ children }) {
return <DjangoContext>{children}</DjangoContext>
}
// page.tsx
import { useEcho, useCurrentUser, DjangoError } from '@/api'
function MyComponent() {
const user = useCurrentUser()
const echo = useEcho()
const handleClick = async () => {
try {
const result = await echo({ text: 'hello' })
console.log(result.message) // typed
} catch (e) {
if (e instanceof DjangoError) {
console.log(e.code) // NOT_FOUND, VALIDATION_ERROR, etc.
}
}
}
}
Features
| Backend | Frontend (generated) | Transport |
|---|---|---|
@client |
useXxx() |
HTTP |
@client(context='global') |
useXxx() + SSR hydration |
HTTP |
@client(context='local') |
useXxx() with params |
HTTP |
@client(websocket=True) |
useXxx() |
WebSocket RPC |
@client(auth=True|'staff'|callable) |
Auth errors as DjangoError |
HTTP |
mizanFormMixin |
useXxxForm() + Zod validation |
HTTP |
ReactChannel |
useXxxChannel() |
WebSocket |
@compose(...) |
Combined providers | varies |
Architecture
React app
└─ <DjangoContext> ← generated provider (includes ChannelProvider)
├─ useCurrentUser() ← generated context hook (SSR-hydrated)
├─ useEcho() ← generated function hook
├─ useContactForm() ← generated form hook (Zod + server validation)
└─ useChatChannel() ← generated channel hook (WebSocket)
│
├─ HTTP: POST /api/mizan/call/ { fn: "echo", args: { text: "hi" } }
└─ WS: { action: "rpc", fn: "echo", args: { text: "hi" } }
│
Django executor
├─ Pydantic input validation
├─ Auth check (session, JWT, or custom)
├─ Function execution
└─ Pydantic output serialization
The generated DjangoContext is the only provider needed. It wraps mizanProvider + ChannelProvider and handles session init, CSRF, context auto-fetching, and WebSocket connection.
Code Generation
npx mizan-generate reads Django schemas (no running server needed) and produces:
| File | Contents |
|---|---|
generated.mizan.ts |
Pydantic model types (via openapi-typescript) |
generated.django.tsx |
DjangoContext provider + all typed hooks |
generated.django.server.ts |
SSR hydration helper (getDjangoHydration) |
generated.forms.ts |
Form hooks with Zod schemas (useContactForm, etc.) |
generated.channels.ts |
Channel message types |
generated.channels.hooks.tsx |
Channel hooks (useChatChannel, etc.) |
index.ts |
Consolidated re-exports |
Error Handling
All errors from server functions are thrown as DjangoError:
try {
await echo({ text: 'hello' })
} catch (e) {
if (e instanceof DjangoError) {
e.code // 'NOT_FOUND' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'FORBIDDEN' | ...
e.message // Human-readable message
e.details // Field-level validation errors, etc.
e.isAuthError()
e.isValidationError()
e.getFieldErrors('email')
}
}
Error codes: NOT_FOUND, VALIDATION_ERROR, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST, INTERNAL_ERROR, NOT_IMPLEMENTED.
Forms
Django forms get typed React hooks with client-side Zod validation:
# Django
class ContactForm(mizanFormMixin, forms.Form):
mizan = mizanFormMeta(
name="contact",
title="Contact Us",
submit_label="Send",
live_validation=True,
)
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
def on_submit_success(self, request):
send_email(self.cleaned_data)
return {"sent": True}
// React (generated)
const form = useContactForm()
form.schema // { fields: { name: {...}, email: {...} }, title, submit_label }
form.data // { name: '', email: '', message: '' }
form.set('email', v) // typed setter
form.errors // field-level errors (Zod + server)
form.submit() // → { success: true, data: { sent: true } }
Channels
WebSocket channels with typed messages:
# Django
class ChatChannel(ReactChannel):
class Params(BaseModel):
room: str
class ReactMessage(BaseModel):
text: str
class DjangoMessage(BaseModel):
text: str
user: str
def authorize(self, params):
return self.user.is_authenticated
def group(self, params):
return f"chat_{params.room}"
def receive(self, params, msg):
return self.DjangoMessage(text=msg.text, user=self.user.email)
// React (generated)
const chat = useChatChannel({ room: 'general' })
chat.status // 'connecting' | 'connected' | 'disconnected'
chat.messages // ChatDjangoMessage[]
chat.send({ text: 'hello' })
Testing
# Django unit tests
cd packages/mizan-django && uv sync --extra dev --extra channels && uv run pytest
# React unit tests
cd packages/mizan-react && npm test
# E2E integration tests (real browser, real backend)
docker compose -f examples/django-react-site/docker-compose.test.yml up -d
cd examples/django-react-site/harness && npm install && npx mizan-generate && npx vite --port 5174 &
npx playwright test
# All at once
make test-all
Project Structure
mizan/
packages/
mizan-runtime/ Client state engine (~150 lines, framework-agnostic)
mizan-django/ Django server adapter (decorators, dispatch, contexts, SSR)
mizan-react/ React adapter (thin wrapper around runtime)
examples/
django-react-site/ E2E tests + Django backend
django-react-desktop-app/ PyWebView desktop app