Architecture

How the magic happens under the hood.

pyRPC is designed to be as "invisible" as possible. To achieve this, it relies on a simple multi-tier architecture: Registry, Introspection, Config, and Contract.

1. The Registry (Source of Truth)

When you use the @rpc decorator, you are adding your function to a central Registry.

@rpc
def add(a: int, b: int) -> int: ...

The registry captures the function's signature using Python's inspect module and Pydantic's TypeAdapter. It knows exactly what inputs it expects and what output it promises.

2. The Introspection Engine

The registry is connected to an Introspection Engine. This engine can turn the Python signatures into a structured JSON schema at any time.

This engine is exposed via the GET /rpc endpoint. It allows tools (and other pyRPC clients) to "see" your backend as if it were a typed library.

3. Project Configuration (pyrpc.json)

pyRPC stores project settings in a dedicated pyrpc.json file:

{
  "backend": {
    "framework": "fastapi",
    "entrypoint": "main:app"
  },
  "clients": [
    { "framework": "Next.js", "root": "../frontend" }
  ]
}
  • backend.framework: The backend framework (fastapi, flask, django, or asgi) - decides which native dev server pyrpc dev launches (uvicorn, flask run, or manage.py runserver)
  • backend.entrypoint: Framework-specific launch target - a module[:app] for FastAPI/Flask/ASGI, the path to manage.py for Django
  • backend.types_module: Optional module whose import registers your @rpc procedures; defaults to the module part of entrypoint
  • clients: One or more TypeScript client project roots, each with its detected frontend framework (used for bundler aliasing)

All paths are resolved relative to pyrpc.json's directory at config load time. The types output path is <client>/__pyrpc.ts, derived automatically from each client root.

The config file is created by pyrpc dev on first run and can be updated with --reconfigure or individual flags (--yes, --framework, --module, --client).

4. Contract Synchronization

The final tier is the Contract. This is the bridge to TypeScript.

Instead of generating a bulky SDK with custom classes and logic, pyrpc codegen (or pyrpc dev) fetches the introspection schema and translates it into a TypeScript runtime module (__pyrpc.ts) written into your client project. It carries both the procedure types and a runtime kind map (procedureKinds) that the framework adapters use to expose useQuery vs useMutation.

pyrpc dev also wires @pyrpc/types to that generated file: a tsconfig.json paths entry ("@pyrpc/types": ["./__pyrpc.ts"]) via jsonc-edit, plus an explicit bundler alias for Vite, SvelteKit, and Next.js Turbopack, which don't honor tsconfig paths for imports inside node_modules.

The Journey of a Request

  1. Client: Calls client.add(1, 2).
  2. Runtime: Packages the call into a JSON-RPC 2.0 object.
  3. Transport: Sends a POST /rpc to the server.
  4. Adapter: (FastAPI/Flask) receives the request and passes it to pyRPC.
  5. Interpreter: Validates the parameters via Pydantic, calls the Python function, and catches any errors.
  6. Response: The result is packaged and sent back to the client.

By understanding this flow, you can see why pyRPC is so reliable - there is no manual translation layer where types can drift.