# DJAREA A modern Django + React Framework for perfectionists with deadlines. Write a Pydantic function, add the @client decorator, use configurable **Shape** types for your models. Djarea generates the entire React client: all your type interfaces, function call hooks, autoatic JWT, and a simple `` to make it all work. No API routing, no serializers, no REST/CRUD bullshit. ```python @client def current_user(request) -> UserShape: return UserShape.query(lambda qs: qs.filter(pk=request.user.pk))[0] ``` ```tsx const user: UserShape = useCurrentUser() // typed, cached, SSR-hydrated ``` The **Function** is the API contract. The **Shape** is the query. The hook is the artifact. That's it. Starts with session auth and upgrades to JWT on login. **It just works**. ## What Djarea does A `@client` function in Django becomes a callable hook in React. The function's type signature orchestrates the entire pipeline for you — input validation, output serialization, TypeScript interfaces, and SQL projection. ```python class ArticleShape(Shape[Article]): id: int | None = None title: str author: FlatAuthorShape tags: list[TagShape] = [] ``` One Djarea **Shape** does three things simultaneously: - Defines the Pydantic model for validation and serialization - Generates a django-readers spec for a lean, field-scoped SQL query - Produces the TypeScript interface on the React side Shapes are your codebase's **single source of truth** for backend/frontend data transfer. ## Quick start ### 1. Django setup ```python # settings.py INSTALLED_APPS = [ "djarea", "myapp", ] # urls.py from django.urls import include, path urlpatterns = [ path("api/djarea/", include("djarea.urls")), ] # asgi.py (for WebSocket support) from djarea import wrap_asgi from django.core.asgi import get_asgi_application application = wrap_asgi(get_asgi_application()) ``` ### 2. Define your client functions ```python # myapp/clients.py from djarea.client import client from djarea.shapes import Shape from pydantic import BaseModel class EchoOutput(BaseModel): message: str @client def echo(request, text: str) -> EchoOutput: return EchoOutput(message=text) ``` Functions in `clients.py` are discovered automatically — same convention as `models.py`. ### 3. Generate TypeScript To get your generated React client, set this up in your frontend root: ```javascript // django.config.mjs export default { source: { django: { managePath: '../backend/manage.py', command: ['uv', 'run', 'python'], }, }, output: 'src/api/generated.ts', } ``` Run this command everytime your client needs updating. You can also throw this it on a file watcher pointed at your backend code: ```bash npx djarea-generate ``` ### 4. Use in React ```tsx import { DjangoContext, useEcho, useCurrentUser, DjangoError } from '@/api' // layout.tsx — one provider, handles everything export default function Layout({ children }) { return {children} } // page.tsx 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. e.getFieldErrors('email') // field-level errors } } } } ``` ## Shapes Shapes are Djarea's data protocol. A Shape defines exactly which fields to select from the database, validated through Pydantic and projected through django-readers. Different views get different Shapes — same model, different queries. ```python # Full detail page — joins books with chapters class AuthorDetailShape(Shape[Author]): id: int | None = None name: str bio: str books: list[BookShape] = [] # Dropdown menu — two columns, no joins class FlatAuthorShape(Shape[Author]): id: int | None = None name: str ``` ```python # Detail page: SELECT id, name, bio + prefetch books authors = AuthorDetailShape.query() # Dropdown: SELECT id, name. That's it. authors = FlatAuthorShape.query() ``` Shapes also support diffing. When the frontend sends state back, the diff system compares incoming data against the current database state and tells you exactly what changed: ```python @client def update_articles(request, articles: list[ArticleShape]) -> dict: for article, diff in ArticleShape.diff_many(articles): if diff.is_new: create_article(article) elif diff.changed: update_fields(article, diff.changed) for tag in diff.tags.created: add_tag(article, tag) for tag_id in diff.tags.deleted: remove_tag(article, tag_id) return {"ok": True} ``` One query fetches all current state. The diff is per-field and per-nested-relation. Your service code only touches what actually changed. ## The `@client` decorator The decorator controls transport, caching, auth, and SSR behavior: | Decorator | React hook | What it does | |-----------|-----------|--------------| | `@client` | `useEcho()` | HTTP call, returns typed result | | `@client(context='global')` | `useCurrentUser()` | Fetched once, cached in context, SSR-hydrated | | `@client(context='local')` | `useArticle({ id })` | Cached per unique params | | `@client(websocket=True)` | `useSearch()` | Runs over WebSocket instead of HTTP | | `@client(auth=True)` | — | Requires authentication | | `@client(auth='staff')` | — | Requires staff status | | `@client(auth=my_check)` | — | Custom auth callable | ## Forms Django forms become typed React hooks with client-side Zod validation: ```python class ContactForm(DjareaFormMixin, forms.Form): djarea = DjareaFormMeta( 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} ``` ```tsx const form = useContactForm() form.schema // field metadata, 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 } } ``` Zod schemas are generated from the Django form definition. Validation runs client-side first, server-side second. No duplicated validation logic. ## Channels WebSocket channels with typed messages: ```python 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) ``` ```tsx const chat = useChatChannel({ room: 'general' }) chat.status // 'connecting' | 'connected' | 'disconnected' chat.messages // ChatDjangoMessage[] chat.send({ text: 'hello' }) ``` ## Architecture ``` React app └─ ← generated provider (session, CSRF, WebSocket) ├─ useCurrentUser() ← context hook (SSR-hydrated) ├─ useEcho() ← function hook ├─ useContactForm() ← form hook (Zod + server validation) └─ useChatChannel() ← channel hook (WebSocket) │ ├─ HTTP: POST /api/djarea/call/ { fn: "echo", args: { text: "hi" } } └─ WS: { action: "rpc", fn: "echo", args: { text: "hi" } } │ Django executor ├─ Pydantic input validation ├─ Auth check ├─ Function execution └─ Pydantic output serialization ``` All transport goes through a single endpoint. The generated `DjangoContext` is the only provider. It handles session init, CSRF, context auto-fetching, and WebSocket connection. ## Code generation `npx djarea-generate` reads Django schemas at build time (no running server) and produces: | File | Contents | |------|----------| | `generated.djarea.ts` | Pydantic model types | | `generated.django.tsx` | `DjangoContext` provider + typed hooks | | `generated.django.server.ts` | SSR hydration helper | | `generated.forms.ts` | Form hooks with Zod schemas | | `generated.channels.ts` | Channel message types | | `generated.channels.hooks.tsx` | Channel hooks | | `index.ts` | Re-exports | ## Error handling All errors from server functions throw as `DjangoError`: ```tsx if (e instanceof DjangoError) { e.code // 'NOT_FOUND' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | ... e.message // human-readable e.details // field-level validation errors e.isAuthError() e.isValidationError() e.getFieldErrors('email') } ``` ## Why RPC instead of REST REST exposes your database tables as CRUD endpoints and pushes business logic to the frontend. "Submit an application" becomes PATCH one resource, POST another, PUT a third — choreographed by client code. Djarea keeps business logic on the server. You write functions that do things. The frontend calls them. The server knows what "submit" means. The client doesn't need to. If you delete the frontend of a REST app, your backend is a database. If you delete the frontend of a Djarea app, your backend still has your entire application logic. ## Packages | Package | Install | |---------|---------| | `djarea` (Python) | `pip install djarea` | | `@rythazhur/djarea` (TypeScript) | `npm install @rythazhur/djarea` | For WebSocket support: `pip install "djarea[channels]"` ## Testing ```bash # Django cd django && uv run pytest # React cd react && npm test # E2E (Playwright, real browser + real backend) docker compose -f docker-compose.test.yml up -d cd e2e/harness && npx djarea-generate && npx playwright test # Everything make test-all ``` ## Project structure ``` djarea/ django/ Python package react/ TypeScript package example/ Integration test backend e2e/ Playwright E2E tests Makefile Test orchestration ```