← Back to Blog

Django + Next.js: RSC prefetch with a Django backend

·11 min read

Django + Next.js is a production-grade combination: Django's admin, ORM, and auth on the backend; Next.js App Router with RSC prefetch on the frontend. pyRPC handles the contract between them, no API layer to maintain, no schema drift.

Server

# myproject/views.py
from pyrpc_core import rpc

@rpc.query
async def greet(name: str = "World") -> dict:
    return {"message": f"Hello, {name}!", "framework": "Django"}

@rpc.query
async def read_item(item_id: int, q: str = None) -> dict:
    return {"item_id": item_id, "q": q}

@rpc.mutation
async def create_item(name: str, description: str = None) -> dict:
    return {"name": name, "description": description, "created": True}

# myproject/urls.py
from . import views  # required to register @rpc decorators
from pyrpc_django import mount_django
urlpatterns = [
    path("", views.index),
]
mount_django(urlpatterns)

# myproject/settings.py
CORS_ALLOWED_ORIGINS = ["http://localhost:3000"]

Start pyrpc dev

cd server
uv add pyrpc-core[django]
pyrpc dev --yes --module myproject.views --client ../client

Client

// lib/pyrpc.ts
import { createNextClient } from "@pyrpc/next"
import type { Types } from "@pyrpc/types"

export const api = createNextClient<Types>({
  baseUrl: process.env.PYRPC_URL ?? "http://localhost:8000",
})
// app/page.tsx, Server Component
import { api } from "@/lib/pyrpc"
import { Counter } from "./counter"

export default async function Page() {
  await api.prefetch.greet({ name: "Django User" })
  await api.prefetch.read_item({ item_id: 42, q: "django-test" })
  return (
    <api.HydrationBoundary state={api.dehydrate()}>
      <Counter />
    </api.HydrationBoundary>
  )
}

// app/counter.tsx, Client Component
"use client"
export function Counter() {
  const { data: greeting, isLoading } = api.greet.useQuery({ name: "Django User" })
  const { data: item } = api.read_item.useQuery({ item_id: 42, q: "django-test" })
  const createItem = api.create_item.useMutation()
  const [name, setName] = useState("")

  return (
    <div>
      {isLoading ? <p>Loading…</p> : <pre>{JSON.stringify(greeting)}</pre>}
      <pre>{JSON.stringify(item)}</pre>
      <input value={name} onChange={e => setName(e.target.value)} />
      <button onClick={() => createItem.mutate({ name })} disabled={createItem.isPending}>
        {createItem.isPending ? "Creating…" : "Create"}
      </button>
      {createItem.isSuccess && <pre>{JSON.stringify(createItem.data)}</pre>}
    </div>
  )
}

Run it

cd server && pyrpc dev
cd client && npm install && npm run dev

Open http://localhost:3000. Full source at examples/django-nextjs.

Why Django + Next.js?

Django gives you a production-grade admin panel, migrations, the ORM, and auth out of the box. Next.js gives you App Router, RSC, and first-class TypeScript. pyRPC's thin adapter layer means you never write a REST endpoint, Django procedures call through to your models directly, and the types land in Next.js automatically.