pyRPC
← Back to Blog

Building the Next.js example app: file by file

·12 min read

The examples/nextjs directory is a complete working app: FastAPI backend, Next.js App Router frontend, end-to-end type safety. This post walks through every file and what it does.

server.py — the Python backend

from fastapi import FastAPI
from pyrpc_fastapi import mount_fastapi
from pyrpc_core import rpc

app = FastAPI()

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

@rpc.query
def get_user(user_id: int) -> dict:
    return {"id": user_id, "name": "Ada"}

mount_fastapi(app)

Two procedures. mount_fastapi(app) registers the POST /rpc endpoint. That is the entire backend.

lib/pyrpc.ts — the client setup

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

export const { api, prefetch, dehydrate, HydrateBoundary } =
  createNextClient<Types>({
    baseUrl: process.env.NEXT_PUBLIC_PYRPC_URL!,
    kinds: procedureKinds,
  })

One line creates the full client: hooks, prefetch, dehydration, and the hydration boundary. The variable names are yours to choose.

app/layout.tsx — providers

import { api } from "@/lib/pyrpc"

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <api.Provider>{children}</api.Provider>
      </body>
    </html>
  )
}

Wrap once with api.Provider. All child components get access to the hooks.

app/page.tsx — RSC with prefetch

import { prefetch, dehydrate, HydrateBoundary } from "@/lib/pyrpc"
import Greeting from "./greeting"

export default async function Home() {
  await prefetch.greet("Ada")
  const state = dehydrate()

  return (
    <HydrateBoundary state={state}>
      <Greeting />
    </HydrateBoundary>
  )
}

Server Component prefetches data, dehydrates the cache, passes it to the client via HydrateBoundary. No loading spinners for initial data.

app/greeting.tsx — client hook

"use client"
import { api } from "@/lib/pyrpc"

export default function Greeting() {
  const { data } = api.greet.useQuery("Ada")
  return <p>{data ?? "Loading..."}</p>
}

The client component uses useQuery because greet is a query. If it were a mutation, it would show useMutation automatically.

app/providers.tsx — client provider

"use client"
import { api } from "@/lib/pyrpc"

export function Providers({ children }) {
  return <api.Provider>{children}</api.Provider>
}

Separate client provider for areas where the root layout is a Server Component but children need client-side hooks.

Why this works

Every file has a single responsibility. The backend is three lines of Python. The frontend is standard Next.js App Router with TanStack Query underneath. pyRPC is the glue, not the framework.

examples/nextjs on GitHub · Next.js docs