pyRPC
DemoBlogChangelogDocs
← Back to Blog

From raw FastAPI to pyRPC

·7 min read

You have a working FastAPI app. Routes are organized, Pydantic models validate your inputs, OpenAPI documents your endpoints. But every time you add a new endpoint, you need to update your frontend types, write a new fetch call, and make sure the URL matches. It's not broken — it's just manual.

This post walks through migrating a real FastAPI application to pyRPC, showing the before and after for each piece.

Starting point: raw FastAPI

Here's a typical user management API:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    id: int
    name: str
    email: str

class CreateUserInput(BaseModel):
    name: str
    email: str

@app.get("/users/{user_id}")
async def get_user(user_id: int) -> User:
    user = await fetch_user(user_id)
    if not user:
        raise HTTPException(404, "User not found")
    return user

@app.post("/users")
async def create_user(data: CreateUserInput) -> User:
    user = await insert_user(data.name, data.email)
    return user

And the TypeScript client code that goes with it:

interface User {
    id: number
    name: string
    email: string
}

async function getUser(userId: number): Promise<User> {
    const res = await fetch(`/users/${userId}`)
    if (!res.ok) throw new Error(await res.text())
    return res.json()
}

async function createUser(name: string, email: string): Promise<User> {
    const res = await fetch("/users", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, email }),
    })
    if (!res.ok) throw new Error(await res.text())
    return res.json()
}

Every endpoint URL, HTTP method, status code, request shape, and response type is hand-written and manually kept in sync. If you change the Python model, you have to find and update every TypeScript usage.

Step 1: Add pyRPC

pip install pyrpc-core pyrpc-fastapi

That's it. One install command for the core runtime and the FastAPI adapter.

Step 2: Replace routes with procedures

Instead of route decorators, you use @rpc. Instead of BaseModel, you use @model. And instead of running uvicorn app:app directly, you call mount_fastapi(app):

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

app = FastAPI()

@model
class User:
    id: int
    name: str
    email: str

@rpc
def get_user(user_id: int) -> User:
    user = fetch_user(user_id)
    if not user:
        raise ValueError("User not found")
    return user

@rpc
def create_user(name: str, email: str) -> User:
    return insert_user(name, email)

mount_fastapi(app)

A few things to notice:

  • No HTTP boilerplate — No routes, no methods, no status codes. The function is the endpoint.
  • Plain Python types — Primitives are validated automatically. Complex models use @model which is just a thin wrapper over Pydantic.
  • Exceptions become errorsValueError is automatically mapped to an error response. No more HTTPException boilerplate.
  • Sync is fine — pyRPC handles sync functions transparently. No need to make everything async unless you need it.

Step 3: Generate the TypeScript client

npx pyrpc codegen --url http://localhost:8000

This generates a @pyrpc/types package with all your model and procedure types. Then your frontend becomes:

import { createClient } from "@pyrpc/client"
import type { Types } from "@pyrpc/types"

const client = createClient<Types>()

const user = await client.get_user(1)
const newUser = await client.create_user("Alice", "alice@example.com")

Compare this to the original TypeScript code. No fetch calls, no URL strings, no JSON parsing, no manual error handling, no manual types. Everything is inferred.

The diff

Here's what changed per endpoint:

ConcernRaw FastAPIpyRPC
Route@app.get("/users/{id}")@rpc
Input modelclass CreateUser(BaseModel)def create_user(name: str, email: str)
Error handlingraise HTTPExceptionraise ValueError
Client callfetch(...).then(r => r.json())client.get_user(1)
Type safetyManualInferred

When should you migrate?

pyRPC isn't meant to replace every FastAPI endpoint. It's best for:

  • CRUD operations — The bread and butter of most backends.
  • Business logic procedures — Functions that take inputs and return outputs.
  • Internal APIs — Services that call each other within your infrastructure.

It's less suited for file uploads, streaming responses, or endpoints that need fine-grained HTTP control. For those, keep your regular FastAPI routes alongside pyRPC — they coexist perfectly.

Try it yourself

The fastest way to see the difference is to open the interactive playground, write a few @rpc procedures, and watch the TypeScript types update in real time.