Next.js App Router adds one capability that plain React doesn't have: Server Components can prefetch data before the page is sent to the browser. @pyrpc/next plugs directly into that pattern, api.prefetch warms the TanStack cache on the server, and api.HydrationBoundary hands it to the browser. Client components call useQuery and see instant data with no loading state.
Project layout
fastapi-nextjs/
server/
main.py ← FastAPI app (identical to fastapi-react server)
pyrpc.json
client/
lib/pyrpc.ts ← createNextClient
app/
layout.tsx ← RootLayout + Providers
providers.tsx ← "use client" QueryClient + api.Provider
page.tsx ← Server component: prefetch
counter.tsx ← Client component: useQuery/useMutationThe server (unchanged)
The FastAPI server is identical to the React example, the backend doesn't know or care which frontend framework you use.
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_credentials=True, allow_methods=["*"], allow_headers=["*"])
@rpc.query
def read_root(): return {"Hello": "World"}
@rpc.query
def read_item(item_id: int, q: str = None): return {"item_id": item_id, "q": q}
@rpc.mutation
def create_item(name: str, description: str = None):
return {"name": name, "description": description, "created": True}
mount_fastapi(app)Client setup
// 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/providers.tsx
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { api } from "@/lib/pyrpc"
import { useState } from "react"
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<api.Provider>{children}</api.Provider>
</QueryClientProvider>
)
}Server component, prefetch
// app/page.tsx
import { api } from "@/lib/pyrpc"
import { Counter } from "./counter"
export default async function Page() {
// warm the cache before the HTML is sent
await api.prefetch.read_root()
await api.prefetch.read_item({ item_id: 42, q: "test" })
return (
<api.HydrationBoundary state={api.dehydrate()}>
<Counter />
</api.HydrationBoundary>
)
}Client component, hooks
// app/counter.tsx
"use client"
import { api } from "@/lib/pyrpc"
import { useState } from "react"
export function Counter() {
const [name, setName] = useState("")
// already in cache, renders without a loading state
const { data: greeting, isLoading } = api.read_root.useQuery()
const { data: item } = api.read_item.useQuery({ item_id: 42, q: "test" })
const createItem = api.create_item.useMutation()
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, description: `Item: ${name}` })}
disabled={createItem.isPending}>
{createItem.isPending ? "Creating…" : "Create"}
</button>
{createItem.isSuccess && <pre>{JSON.stringify(createItem.data)}</pre>}
</div>
)
}Run it
# Terminal 1 cd server && uv add pyrpc-core[fastapi] && pyrpc dev # Terminal 2 cd client && npm install && npm run dev
Open http://localhost:3000. The greeting and item data render instantly (no loading spinner) because the server prefetched them before shipping the HTML. The create form works client-side as a normal mutation.
When to skip prefetch
Prefetch is optional. If you don't call api.prefetch, useQuery fetches on mount exactly like plain React, still fully typed, just no server warm-up.

pyRPC