pyRPC
← Back to Blog

Tutorial: Next.js App Router + TanStack Query with pyRPC

·14 min read

Build a typed full-stack slice: Python procedures with @rpc.query / @rpc.mutation, a Next.js App Router UI, RSC prefetch, and client hooks — all through @pyrpc/next.

Prefer a ready-made tree? Clone the repo and open examples/nextjs. This tutorial walks the same path from scratch.

What you will build

  • FastAPI + pyRPC API on port 8000
  • Next.js app that prefetches on the server and hydrates on the client
  • A query hook and a mutation hook wired to TanStack Query

Step 1 — Python API

# server.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pyrpc_core import rpc
from pyrpc_fastapi import mount_fastapi

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_methods=["*"],
    allow_headers=["*"],
)

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

@rpc.query
async def get_status() -> dict:
    return {"status": "online", "version": "0.8.1"}

@rpc.mutation
def set_display_name(name: str) -> dict:
    return {"ok": True, "name": name}

mount_fastapi(app)

# uv run uvicorn server:app --reload --port 8000

Bare @rpc still works and defaults to kind query. Prefer explicit @rpc.query / @rpc.mutation for new code.

Step 2 — Generate types

# with the API running
pyrpc codegen  # or: npx pyrpc sync in server distribution mode

You should get Types, ProcedureKinds, and a runtime procedureKinds object in @pyrpc/types.

Step 3 — Install the Next stack

npm install @pyrpc/next @pyrpc/react @pyrpc/client @pyrpc/types @tanstack/react-query
npm install next react react-dom

Step 4 — createNextClient

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

export const {
  api,
  createCaller,
  prefetch,
  dehydrate,
  HydrateClient,
} = createNextClient<Types, typeof procedureKinds>({
  baseUrl: process.env.PYRPC_URL ?? "http://localhost:8000",
  kinds: procedureKinds,
})

Naming matches createReactClient. The Next factory returns a bundle because App Router needs server helpers, not only hooks.

Step 5 — Provider

// app/providers.tsx
"use client"
import { NextPyRPCProvider } from "@pyrpc/next"

export function Providers({ children }: { children: React.ReactNode }) {
  return <NextPyRPCProvider>{children}</NextPyRPCProvider>
}

// app/layout.tsx — wrap children with <Providers>

This is TanStack Query’s cache provider. pyRPC does not add a separate RPC context.

Step 6 — Prefetch in a Server Component

// app/page.tsx
import { dehydrate, HydrateClient, prefetch } from "@/lib/pyrpc"
import { Greeting } from "./greeting"

export default async function Page() {
  await prefetch.greet("Ada")
  await prefetch.get_status(undefined)

  return (
    <HydrateClient state={dehydrate()}>
      <Greeting />
    </HydrateClient>
  )
}

Step 7 — Client Component hooks

// app/greeting.tsx
"use client"
import { api } from "@/lib/pyrpc"

export function Greeting() {
  const { data, isLoading } = api.greet.useQuery("Ada")
  const status = api.get_status.useQuery(undefined)
  const rename = api.set_display_name.useMutation()

  return (
    <div>
      <p>{isLoading ? "…" : data}</p>
      <p>{status.data?.status}</p>
      <button onClick={() => rename.mutate("Grace")}>Rename</button>
    </div>
  )
}

With procedureKinds, set_display_name only exposes useMutation — calling useQuery on it is a type error.

Step 8 — Run both processes

# terminal 1
uv run uvicorn server:app --reload --port 8000

# terminal 2
npm run dev   # Next on :3000

Common pitfalls

  • Missing baseUrl in RSC — Server Components have no window. Always set baseUrl / PYRPC_URL.
  • Forgetting the provider — hooks throw without a QueryClient. Use NextPyRPCProvider or raw QueryClientProvider.
  • CORS — allow http://localhost:3000 on the FastAPI app during local dev.
  • Stale kinds — after changing @rpc.query / @rpc.mutation, regenerate types.

Next reading