Ryth Azhur 1b5dca5ab3 SSR: file-path rendering, no component registry
The worker receives a file path in the JSON message, dynamically
imports it, renders it. No registerComponent API, no app entry file,
no export maps. Django's template backend resolves the template name
to an absolute path against DIRS, same as every other template engine.

  render(request, 'components/Hello.tsx', {'name': 'World'})

Verified working: curl http://localhost:8000/hello/ returns
  <div id="mizan-root"><div>Hello, World!</div></div>

Changes:
- worker.tsx: receives file path, dynamic import with cache
- bridge.py: sends file path instead of component name
- backend.py: resolves template name against DIRS to absolute path
- Fix bridge.py:147 bug (referenced deleted 'component' variable)
- Example app: Hello.tsx component, /hello/ view, template config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 03:33:01 -04:00

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
Description
No description provided
Readme 4.1 MiB
Languages
Python 45.2%
TypeScript 32.2%
Rust 20.8%
Jinja 1.3%
JavaScript 0.3%
Other 0.2%