# Overview pyRPC fits naturally into agentic workflows. These tools help assistants and automation stay aligned with the library and the docs. | Resource | What it is | | --------------------------------------- | --------------------------------------------------------------------------------------- | | [LLMs.txt](/docs/ai-resources/llms-txt) | Machine-friendly index, full-text dump, and per-page Markdown for tools and assistants. | | [MCP](/docs/ai-resources/mcp) | Documentation MCP server for search, examples, and setup in MCP clients. | | [Skills](/docs/ai-resources/skills) | Portable instruction files so agents follow pyRPC conventions. | The site footer also has an **Ask AI about pyRPC** control that opens your assistant (Claude, ChatGPT, Perplexity) with a pre-filled prompt, and every docs page has **Copy MD** actions that grab the page as Markdown for pasting into a conversation. # LLMs.txt The docs are served in plain Markdown forms that tools and assistants can consume directly, no scraping required. | Route | What it is | | ---------------------------------- | -------------------------------------------------------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | A flat index of every page: title, URL, and description. Good for orientation and link discovery. | | [`/llms-full.txt`](/llms-full.txt) | The full content of every page in one Markdown document. Good for offline context or one-shot ingestion. | | `/llms.mdx/docs/.mdx` | The raw Markdown of a single page. Every docs page links to its own version. | ## Per-Page Markdown [#per-page-markdown] Each docs page exposes its raw content by appending its path to `/llms.mdx`: ``` /llms.mdx/docs/get-started/quickstart.mdx /llms.mdx/docs/client/adapters/react.mdx ``` Use the **Copy MD** and **Copy MD Link** actions on any page to grab this without constructing the URL yourself. ## Tips [#tips] * Point an agent at `/llms.txt` first; it is small enough to fit in a prompt and lets the agent decide which pages to fetch as Markdown. * Use `/llms-full.txt` when you want a single artifact for RAG indexing or a local assistant. * Per-page Markdown is stable across deploys, so you can cache it. # MCP pyRPC ships two distinct MCP surfaces. They solve different problems and can be used together. ## Which MCP should I use? [#which-mcp-should-i-use] | | Local Project MCP | Remote Documentation MCP | | --------------------- | -------------------------------------------------------------------------------- | ----------------------------------------- | | Purpose | Your application: routers, procedures, schemas, backend config, generated client | pyRPC itself: docs, APIs, concepts, usage | | Runs | Your machine | pyRPC infrastructure | | Needs Python | Yes | No | | Needs a pyRPC project | Yes | No | | Transport | stdio (subprocess) | Streamable HTTP | | Install | `uv add "pyrpc-core[mcp]"` then `pyrpc mcp` | `npx @pyrpc/mcp mcp` | | Server | launched by your client from your project | `https://mcp.pyrpc.com/mcp` | An agent can have both configured at once. The local one understands "my application"; the remote one understands "pyRPC". ## Remote Documentation MCP [#remote-documentation-mcp] The hosted documentation server exposes read-only search and retrieval over the entire pyRPC documentation. No Python installation and no pyRPC project are required. ```bash npx @pyrpc/mcp mcp ``` This command configures your AI coding client to use: ``` https://mcp.pyrpc.com/mcp ``` It is a thin convenience wrapper around the [add-mcp](https://github.com/neon-solutions/add-mcp) configuration engine, which maintains native support for 19 coding agents including Claude Code, Cursor, VS Code, OpenCode, Windsurf, Codex, Zed, Antigravity, Cline, and Gemini CLI. add-mcp is the configuration utility; it is not the MCP server. Useful flags: ```bash npx @pyrpc/mcp mcp --global # user-level instead of project-level npx @pyrpc/mcp mcp --agent cursor # configure a specific agent npx @pyrpc/mcp mcp --list # supported agents ``` Prefer the upstream tool directly? It is the same result: ```bash npx add-mcp https://mcp.pyrpc.com/mcp ``` ### Claude Desktop [#claude-desktop] `claude_desktop_config.json` only accepts local stdio servers. For remote servers, Claude Desktop uses connectors configured in the app: **Settings -> Connectors -> Add custom connector**, then paste `https://mcp.pyrpc.com/mcp`. The connection is brokered through Anthropic's cloud per their custom-connector model. ### Manual configuration [#manual-configuration] Enterprise teams that prefer not to run installer utilities can paste an entry directly. **Claude Code** (`.mcp.json`) ```json { "mcpServers": { "pyrpc-docs": { "type": "http", "url": "https://mcp.pyrpc.com/mcp" } } } ``` **Cursor** (`.cursor/mcp.json`) ```json { "mcpServers": { "pyrpc-docs": { "url": "https://mcp.pyrpc.com/mcp" } } } ``` **VS Code** (`.vscode/mcp.json`) ```json { "servers": { "pyrpc-docs": { "type": "http", "url": "https://mcp.pyrpc.com/mcp" } } } ``` **OpenCode** (`opencode.json`) ```json { "mcp": { "pyrpc-docs": { "type": "remote", "url": "https://mcp.pyrpc.com/mcp" } } } ``` **Windsurf** (`~/.codeium/windsurf/mcp_config.json`) ```json { "mcpServers": { "pyrpc-docs": { "serverUrl": "https://mcp.pyrpc.com/mcp" } } } ``` Reload your agent after any manual change. ## Local Project MCP [#local-project-mcp] The local server runs inside your project's Python environment, imports your configured backend module, and answers from the live registry, giving agents ground truth about *your* application. ```bash uv add "pyrpc-core[mcp]" ``` Then register it with your agent: **Claude Code** ```bash claude mcp add pyrpc -- pyrpc mcp ``` **Cursor** (`.cursor/mcp.json`) ```json { "mcpServers": { "pyrpc": { "command": "pyrpc", "args": ["mcp"] } } } ``` **VS Code** (`.vscode/mcp.json`) ```json { "servers": { "pyrpc": { "command": "pyrpc", "args": ["mcp"] } } } ``` ### Tools [#tools] | Tool | Read-only | What it gives the agent | | -------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `introspect_project` | yes | Backend framework, entrypoint, every registered procedure with kind, parameters, types, requiredness, defaults, docstrings, and input/output JSON Schemas | | `check_call` | yes | Whether hypothetical arguments would be accepted by a procedure, validated against real Python types with per-parameter errors. Nothing is executed | | `run_codegen` | when `dry_run=true` (default) | Regenerates each configured client's `__pyrpc.ts`; dry run reports up to date / would update / would create | ### Security model [#security-model] * **No procedure execution.** There is no tool that invokes your backend code, so agents cannot cause database writes, network calls, or other side effects through pyRPC's MCP. * **Local-only.** The process is spawned by your own client in your project environment. No telemetry, no network egress. * **Narrow writes.** `run_codegen` writes generated files only; tsconfig/bundler setup stays with `pyrpc init` / `pyrpc codegen`. * **Structured errors.** Missing or ambiguous configuration produces actionable errors (including what was detected) rather than guesses. ### How it works [#how-it-works] ``` Claude / Cursor / VS Code / OpenCode | launches subprocess v pyrpc mcp | imports your backend module v your routers, registry, schemas ``` The server must run in the same Python environment as your project because it imports your code. That is automatic when clients spawn `pyrpc mcp` from the project root. ## Example interaction [#example-interaction] With both servers connected, an agent grounds its work in reality: > **Agent:** calls `introspect_project` on the local server. > > ```json > { > "framework": "fastapi", > "procedures": [ > { "name": "get_post", "kind": "query", > "parameters": [{ "name": "id", "required": true }] } > ] > } > ``` > > **Agent:** unsure how mutation invalidation works, searches the remote > docs server for `mutation invalidation react`, reads the adapter guide via > `get_doc`, and follows the documented pattern. > > **Agent:** verifies the payload with > `check_call("get_post", { "id": "abc" })` before writing client code: > > ```json > { "valid": false, "errors": [{ "param": "id", > "message": "Input should be a valid integer" }] } > ``` > > **Agent:** fixes the payload, confirms types are current with > `run_codegen(dry_run=true)`, regenerates with `dry_run=false`. ## Troubleshooting [#troubleshooting] | Symptom | Cause and fix | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `No pyrpc.json found` | Run the client from your project root (or configure `cwd`) so config discovery finds `pyrpc.json`. | | `has no valid 'backend' section` | Set `backend.framework` and `backend.entrypoint`; run `pyrpc init` to generate them interactively. The error lists what was detected. | | Django: `must set backend.types_module` | Add `"types_module": "myproject.views"` (the module whose import registers your procedures). | | `Failed to import backend module ...` | A dependency of that module is missing from the environment running the MCP server. Verify `pyrpc dev` works in the same checkout. | | Client shows no tools | Confirm `uv add "pyrpc-core[mcp]"` succeeded; plain installs print a remediation hint on stderr when `pyrpc mcp` starts without it. | # Skills [Agent skills](https://agentskills.io) are portable instruction files (for example `SKILL.md`) that teach your coding agent project conventions, safe patterns, and where to look in the docs. The **pyRPC** skill pack covers server setup, adapters, client configuration, and codegen. Install it with the [`skills` CLI](https://www.npmjs.com/package/skills) (uses `npx` so nothing global is required): ```bash title="terminal" npx skills add pyrpc ``` Your editor or agent loads skills from its configured skills directory (often project-level). After installing, restart the agent or reload skills if your tool requires it. # Contributing We welcome contributions of all sizes, from fixing a typo to adding a new adapter. ## Getting Started [#getting-started] See [CONTRIBUTING.md](https://github.com/pyrpc/pyrpc/blob/main/CONTRIBUTING.md) on GitHub for the full guide covering: * Development setup (Python 3.11+, Node.js 20+) * Repository structure * Branch naming conventions (`feat/`, `fix/`, `chore/`, `docs/`) * Commit message format (Conventional Commits) * PR template and review expectations ## Quick Start [#quick-start] ```bash git clone https://github.com/pyrpc/pyrpc cd pyrpc uv sync cd docs && npm install && cd .. ``` ## Scope [#scope] Keep PRs focused on one subsystem, core, adapter, client, codegen, or docs. Mixed-concern PRs are hard to review and prone to regressions. ## Questions [#questions] Open a [Discussion](https://github.com/pyrpc/pyrpc/discussions) for questions or feature ideas before opening a PR. # Sponsors pyRPC is an MIT-licensed open source project maintained by [@atnatewoss](https://github.com/atnatewoss). ## How to Support [#how-to-support] * **Star the repo** on [GitHub](https://github.com/pyrpc/pyrpc) * **Report bugs** and suggest features via [Issues](https://github.com/pyrpc/pyrpc/issues) * **Contribute code**: see the [Contributing guide](/docs/community/contributing) * **Sponsor financially**: reach out on [Telegram](https://t.me/pyrpc) to discuss sponsorship options ## Current Sponsors [#current-sponsors] *None yet, be the first!* # Advanced # Advanced Client Usage [#advanced-client-usage] This page covers patterns that go beyond the basic client examples. ## Testing with ASGITransport [#testing-with-asgitransport] You can test the Python client against the in‑memory ASGI app instead of a real server. ```python import httpx import pytest from pyrpc_core import rpc, asgi_app, RPCClient, default_router @pytest.fixture(autouse=True) def clear_registry(): default_router._procedures.clear() @pytest.mark.anyio async def test_client_async(): @rpc def add(a: int, b: int) -> int: return a + b async with RPCClient("http://test") as client: client._async_client = httpx.AsyncClient( transport=httpx.ASGITransport(app=asgi_app), base_url="http://test", ) result = await client.add.aio(10, 20) assert result == 30 ``` ## Custom HTTP Settings [#custom-http-settings] `RPCClient` uses `httpx.Client` / `httpx.AsyncClient` under the hood. You can override them: ```python import httpx from pyrpc_core import RPCClient client = RPCClient("http://localhost:8000") client._sync_client = httpx.Client( base_url="http://localhost:8000", timeout=10.0, headers={"X-Request-Source": "batch-job"}, ) ``` For TypeScript, configure transport via a link. `httpLink` sends one operation per HTTP request; `httpBatchLink` combines concurrent operations into one request. See the [TypeScript client](/docs/client/typescript) for details. ## Codegen Tips [#codegen-tips] * Run `pyrpc dev` in the server directory to regenerate types automatically whenever you save a `.py` file. * Or use the CLI directly: `pyrpc codegen http://localhost:8000 --client ../frontend` (included with `pyrpc-core`). `pyrpc codegen` accepts a running server URL, a saved schema file, or a Python module. * By default, `pyrpc codegen` writes `__pyrpc.ts` to the client project root. Pass `--client ` to point it elsewhere. * Commit the generated `__pyrpc.ts` to your repo so frontend builds don't depend on a running Python server. ## Error Handling Patterns [#error-handling-patterns] * Wrap RPC calls in a small helper that normalizes `RPCError` / `PyRPCError`. * Map error codes to user‑facing messages in one place. ```ts import { PyRPCError } from "@pyrpc/client"; export function handleRpcError(e: unknown): string { if (e instanceof PyRPCError) { if (e.code === -32601) return "Method not found."; if (e.code === -32603) return "Internal server error."; return e.message; } return "Something went wrong. Please try again."; } ``` ## Next Steps [#next-steps] * [Server](/docs/server) - How procedures are defined * [Plugins](/docs/plugins) - Framework‑specific integrations # Overview # Client [#client] This section shows how to call your pyRPC server from different client environments. ## What You'll Learn [#what-youll-learn] * **[TypeScript](/docs/client/typescript)** - End-to-end typed contracts with full IDE autocompletion. * **[Links](/docs/client/links)** - Transport configuration: `httpLink` and `httpBatchLink`. * **[Adapters](/docs/client/adapters/react)** - React, Next.js, Vue, and Svelte on TanStack Query. * **[Vanilla Python](/docs/client/vanilla)** - Dynamic, codegen-free client for scripts and microservices. * **[Advanced](/docs/client/advanced)** - Testing, customization, and patterns. ## Quick Example (Python) [#quick-example-python] ```python from pyrpc_core import RPCClient with RPCClient("http://localhost:8000") as client: result = client.add(a=10, b=5) print(result) # 15 ``` If your server exposes more procedures (via `@rpc`), they appear as dynamic methods on `client`. # TypeScript # TypeScript Client [#typescript-client] The `@pyrpc/client` package provides a lightweight, framework-agnostic runtime for calling your RPC procedures. Combined with generated **Typed Contracts**, it provides a first-class developer experience with zero boilerplate. ## 1. Install [#1-install] ```bash npm install @pyrpc/client ``` `@pyrpc/client` is a thin fetch-based runtime with no codegen or postinstall step. The TypeScript types come from the `__pyrpc.ts` file that `pyrpc dev` generates in your client project root, `import type { Types } from "@pyrpc/types"` resolves to it via a tsconfig path alias that `pyrpc dev` configures automatically (and via a bundler alias for Vite, SvelteKit, and Next.js Turbopack). If you need to regenerate types later (e.g. after adding procedures), just save your `.py` file while `pyrpc dev` is running, or run the CLI directly: ```bash pyrpc codegen http://localhost:8000 --client . ``` ## 2. Create the Client [#2-create-the-client] Use the `createClient()` factory to initialize your typed client. Transport is configured with a **link**, the URL belongs to the link, not the client. ```ts import { createClient, httpBatchLink } from "@pyrpc/client" import type { Types } from "@pyrpc/types" // Multiple operations share one HTTP request export const client = createClient({ links: [ httpBatchLink({ url: "https://api.example.com", }), ], }); ``` ## 3. Transport [#3-transport] The transport is configured with a **link** — see [Links](/docs/client/links) for the full reference (`httpLink`, `httpBatchLink`, options, and batching semantics). ## 4. Call Procedures [#4-call-procedures] Procedures are available as async methods with full autocompletion and type validation. ```ts // Inside a React component or Server Action const user = await client.get_user({ id: 1 }); console.log(user.name); // Typed as string! ``` ## Error Handling [#error-handling] The client throws `PyRPCError` for structured server-side errors. See [Error Handling](/docs/reference/error-handling) for the full guide. ```ts import { PyRPCError } from "@pyrpc/client"; try { const result = await client.add(10, 20); } catch (e) { if (e instanceof PyRPCError) { console.error(`Error ${e.code}: ${e.message}`); } } ``` ## Next Steps [#next-steps] * [Links](/docs/client/links) - HTTP Link and HTTP Batch Link * [Adapters](/docs/client/adapters/react) - React, Next.js, Vue, and Svelte * [Vanilla Python Client](/docs/client/vanilla) # Vanilla Python # Vanilla Python Client [#vanilla-python-client] The Python client gives you a dynamic, ergonomic way to call your pyRPC server. ## RPCClient Basics [#rpcclient-basics] ```python from pyrpc_core import RPCClient, RPCError with RPCClient("http://localhost:8000") as client: try: result = client.add(10, 5) print(result) except RPCError as e: print(f"RPC failed: {e.code} {e.message}") ``` * `RPCClient(base_url)` points at your server (e.g. `http://localhost:8000`). * Each registered procedure (`@rpc def add(...)`) becomes a method on `client`. ## Sync vs Async [#sync-vs-async] By default, calling `client.add(...)` is **synchronous**. For async usage, either: ```python result = await client.add.aio(a=1, b=2) ``` or call the explicit API: ```python result = await client.call_async("add", a=1, b=2) ``` ## Positional vs Keyword Params [#positional-vs-keyword-params] pyRPC supports both styles: ```python client.add(1, 2) # positional -> [1, 2] client.add(a=1, b=2) # keyword -> {"a": 1, "b": 2} ``` On the server, they both map to `def add(a: int, b: int) -> int`. ## Lifetime Management [#lifetime-management] `RPCClient` manages an underlying `httpx` client (sync + async). * Use `with RPCClient(...)` for sync code. * Use `async with RPCClient(...)` for async code. ```python async with RPCClient("http://localhost:8000") as client: status = await client.get_status.aio() ``` You can also close it manually: ```python client = RPCClient("http://localhost:8000") try: ... finally: client.close() ``` ## Error Handling [#error-handling] Server-side errors become `RPCError` instances: ```python from pyrpc_core import RPCError try: client.fail() except RPCError as e: print(e.code, e.message) ``` HTTP errors (non‑2xx) raise `httpx` exceptions before pyRPC even sees the payload. ## Next Steps [#next-steps] * [Next.js / TypeScript Client](/docs/client/adapters/nextjs) * [Advanced Usage](/docs/client/advanced) # Comparison pyRPC brings tRPC-level type safety to Python + TypeScript stacks. It syncs your Python procedure signatures into TypeScript contracts automatically -- no OpenAPI schemas, no codegen pipelines, no manual boilerplate. ## At a Glance [#at-a-glance] | Feature | pyRPC | tRPC | gRPC | | --------------------- | --------------------------- | ----------------------- | ----------------- | | Backend language | Python | TypeScript | Multi-language | | Frontend types | Auto-generated TypeScript | Built-in TypeScript | Generated stubs | | Schema / IDL required | None (Python types) | None (TypeScript types) | `.proto` files | | Framework-agnostic | Yes (ASGI) | Node.js only | Yes (polyglot) | | Transport | HTTP/JSON | HTTP/JSON | HTTP/2 (binary) | | Works in monorepo | Yes | Yes | Yes | | Type sync | Auto-generated `__pyrpc.ts` | Built-in types | Generated stubs | | Setup time | One command | One command | Proto compilation | ## Key Differences [#key-differences] ### vs tRPC [#vs-trpc] * **pyRPC**: Python backend, TypeScript frontend, shared types. Works across language boundaries. * **tRPC**: TypeScript-only. Requires your backend to be Node.js. ### vs gRPC [#vs-grpc] * **pyRPC**: Zero-config. No `.proto` files or IDLs. Uses standard Python functions as the source of truth over HTTP/JSON. * **gRPC**: Requires strict IDLs (Protocol Buffers) and binary serialization. More performant for inter-service communication, but carries much higher DX overhead for web-to-server apps. ## When to Use pyRPC [#when-to-use-pyrpc] ✅ You want tRPC-like type safety but your backend runs Python, not Node.js.\ ✅ You're shipping a React / Next.js frontend with a Python backend and want shared types.\ ✅ You need a drop-in solution for an existing FastAPI or Flask app.\ ✅ You prefer standard Python type hints over OpenAPI schemas or gRPC protos. ### When NOT to Use pyRPC [#when-not-to-use-pyrpc] ❌ Your backend is Node.js -- tRPC is the native choice and will always be ahead.\ ❌ You need strict schema governance with a single IDL source of truth -- gRPC's proto-first workflow may suit you better.\ ❌ You are building a public REST API for third-party consumption -- OpenAPI remains the universal standard for that use case.\ ❌ Your frontend is not TypeScript -- pyRPC's type safety is built for the TS ecosystem. ```python # pyRPC: Your function is the API from pyrpc_core import rpc @rpc async def get_user(user_id: int) -> User: return await db.get_user(user_id) ``` # Introduction
PyPI - Version PyPI - Downloads
**pyRPC** is a drop-in RPC layer for Python. It turns your backend into a library of typed async functions that can be called from anywhere. Inspired by the DX of **tRPC**, pyRPC eliminates the need for OpenAPI schemas, heavy generators, or manual boilerplate. ## The Problem [#the-problem] You rename a Python function from `get_user` to `fetch_user`. Your TypeScript client silently breaks - no error, no warning, just a runtime failure in production. That's the reality of maintaining type safety across a Python backend and a TypeScript frontend without a shared contract layer. ## The Solution [#the-solution] **pyRPC** treats your backend as a **shared library**. It synchronizes your Python procedure signatures directly into TypeScript contracts. * **No OpenAPI**: No intermediate spec files to maintain or sync. * **No SDKs**: No heavy generated code - just clean TypeScript contracts. * **Pure Types**: Your procedures, fully typed, from end-to-end. ## Key Features [#key-features] * **Python-native**: Built for asyncio, Pydantic v2, and modern type hints. * **End-to-End Typing**: Request, response, and errors stay in sync automatically. * **One-command setup**: `pyrpc dev` walks you through configuration and starts everything. * **Modular**: Only install what you need (core, adapters). ## How it works [#how-it-works] 1. **Define**: Use the `@rpc` decorator on any Python function. 2. **Run**: `pyrpc dev` creates a `pyrpc.json` config, starts the server, and generates types. 3. **Call**: Use `createClient()` on the frontend with full type safety. Head to the [Quickstart](/docs/get-started/quickstart) to get a working server and client running in under two minutes. # Installation pyRPC follows a modular packaging strategy. You only pay for what you use. ## Project Setup [#project-setup] ### Python [#python] Before installing pyRPC, set up a Python virtual environment: ```bash # Create a virtual environment python -m venv .venv # Activate it # macOS / Linux: source .venv/bin/activate # Windows: .venv\Scripts\activate ``` If you're using **uv** (recommended), you can initialize a new project: ```bash # Initialize a new uv project (which creates the virtual environment) uv init # Or if your project already exists, just create the venv uv venv # Activate it # macOS / Linux: source .venv/bin/activate # Windows: .venv\Scripts\activate ``` ### TypeScript / npm [#typescript--npm] For the frontend client, you'll need a TypeScript project. Create one if you haven't already: ```bash # Create a new TypeScript project npm init -y npm install typescript --save-dev npx tsc --init ``` The `@pyrpc/client` package and your framework adapter are installed later via `npm` when you set up the frontend. ## Core Package [#core-package] The tiny core protocol and runtime. This is always required. Make sure your virtual environment is activated first, then: ```bash # Using uv uv add pyrpc-core # Using pip pip install pyrpc-core ``` ## Adapters [#adapters] Install the adapter for your favorite web framework. ### FastAPI [#fastapi] ```bash # Using uv uv add pyrpc-core[fastapi] # Using pip pip install pyrpc-core[fastapi] ``` ### Flask [#flask] ```bash # Using uv uv add pyrpc-core[flask] # Using pip pip install pyrpc-core[flask] ``` ### Django [#django] ```bash # Using uv uv add pyrpc-core[django] # Using pip pip install pyrpc-core[django] ``` ## CLI & Code Generation [#cli--code-generation] The `pyrpc` CLI comes built-in with `pyrpc-core` - no separate install needed. ### Dev Server (Recommended) [#dev-server-recommended] The fastest way to get started: ```bash pyrpc dev ``` On first run, `pyrpc dev` walks you through setup interactively - backend framework and entry point, client project root, and frontend framework. It creates a `pyrpc.json` config file, generates types, and starts the development server with auto-regeneration on file changes. The entry point is framework-specific: FastAPI/Flask/ASGI take a `module[:app]` target, Django takes the path to `manage.py`. You can also pass flags to skip the wizard: ```bash pyrpc dev --yes # sniff the framework, auto-detect everything pyrpc dev --yes --framework fastapi --module main --client ../frontend ``` ### Configuration (`pyrpc.json`) [#configuration-pyrpcjson] pyRPC stores its project configuration in a dedicated `pyrpc.json` file (not in `pyproject.toml`): ```json { "backend": { "framework": "fastapi", "entrypoint": "main:app" }, "clients": [ { "framework": "Next.js", "root": "../frontend" } ] } ``` * `backend.framework` - one of `fastapi`, `flask`, `django`, `asgi`. It decides which native dev server `pyrpc dev` launches (uvicorn, `flask run`, or `manage.py runserver`). * `backend.entrypoint` - what the dev server launches: a `module[:app]` target for FastAPI/Flask/ASGI, or the path to your `manage.py` for Django. * `backend.types_module` - optional; the module whose import registers your `@rpc` procedures. Defaults to the module part of `entrypoint`. Required for Django (e.g. the `views.py` module that declares your procedures). * `clients[].root` - one or more frontend project roots; types are written to each. Paths are resolved relative to the config file's directory. The file is created automatically by `pyrpc dev`. ### Manual Codegen [#manual-codegen] For CI/CD or one-off generation: ```bash pyrpc codegen http://localhost:8000 --client ../frontend ``` `pyrpc codegen` accepts a running server URL, a saved schema file, or a Python module, and writes `__pyrpc.ts` to the client project root. ## Quick Start Example [#quick-start-example] ```python test from pyrpc_core import rpc, model @model class User: name: str age: int @rpc def add(a: int, b: int) -> int: return a + b @rpc def greet(user: User) -> str: return f"Hello, {user.name}!" ``` ## Next [#next] * [Quickstart](/docs/get-started/quickstart) - Full server + client example * [Concepts](/docs/concepts) - Mental model and protocol design # Quickstart ## 1. Install [#1-install] ```bash uv add pyrpc-core ``` > `uvicorn` is included as a dependency of `pyrpc-core` - no need to install it separately. The `pyrpc dev` command uses it under the hood. ## 2. Server [#2-server] Create `server.py`: ```python test from pyrpc_core import rpc, model @model class User: name: str age: int @rpc def add(a: int, b: int) -> int: """Add two numbers.""" return a + b @rpc def greet(user: User) -> str: """Greet a user.""" return f"Hello, {user.name}!" ``` ## 3. Start the dev server [#3-start-the-dev-server] Run `pyrpc dev` - on first run it walks you through setup: your backend framework and entry point, then the client project root and frontend framework. The backend framework decides which native dev server runs (uvicorn for FastAPI/ASGI, `flask run` for Flask, `manage.py runserver` for Django), so pick the one your project actually uses. ```bash pyrpc dev ``` **First run output:** ``` pyRPC setup (runs once — saved to pyrpc.json) ? Backend framework: FastAPI ? Backend entry point (module[:app] — the file that calls mount_fastapi): server ? Client project root: ../frontend ? Frontend framework: Next.js ✓ types generated (2 procs) → ../frontend pyRPC dev http://127.0.0.1:8000/rpc ``` The client root prompt autocompletes directories as you type (Tab accepts a suggestion). This creates a `pyrpc.json` config file and starts the dev server with auto-type regeneration on file changes. `pyrpc dev` writes `__pyrpc.ts` to your client project root and wires `@pyrpc/types` to it automatically. To skip the wizard in CI or on repeat setups: ```bash pyrpc dev --yes # sniff the framework, auto-detect module and client pyrpc dev --yes --framework fastapi --module server --client ../frontend pyrpc dev --yes --framework flask --module app --client ../frontend ``` With `--yes`, pyRPC sniffs your code for `mount_fastapi` / `mount_flask` / `mount_django` and refuses to guess when detection fails — pass `--framework` explicitly to be sure. ## 4. Client (TypeScript) [#4-client-typescript] ```ts import { createClient, httpBatchLink } from "@pyrpc/client" import type { Types } from "@pyrpc/types" const api = createClient({ links: [ httpBatchLink({ url: "http://localhost:8000", }), ], }) const result = await api.add(10, 5) const message = await api.greet({ name: "pyRPC", age: 1 }) console.log(result, message) ``` ## 5. Client (Python) [#5-client-python] You can also call your procedures from other Python services or scripts with zero codegen required. ```python from pyrpc_core import RPCClient with RPCClient("http://localhost:8000") as client: # Everything is dynamic and introspected at runtime result = client.add(a=10, b=5) print(result) # 15 ``` ## Done [#done] You now have a working pyRPC server + end-to-end typed contracts. Next: * [Installation](/docs/get-started/installation) - Adapters and CLI tools * [Concepts](/docs/concepts) - Mental model and error handling * [Server Guide](/docs/server) - Routers and procedures * [Client Guide](/docs/client) - TypeScript and Python usage # Architecture 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) [#1-the-registry-source-of-truth] When you use the `@rpc` decorator, you are adding your function to a central **Registry**. ```python @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 [#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`) [#3-project-configuration-pyrpcjson] pyRPC stores project settings in a dedicated `pyrpc.json` file: ```json { "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 `/__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 [#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 [#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. # Introduction # Concepts [#concepts] Before diving into advanced usage, it helps to understand how pyRPC thinks about RPC. ## What You'll Learn [#what-youll-learn] * **Mental Model** - Procedures as typed functions, no OpenAPI middle layer * **Procedures** - Queries vs mutations, the registry, and how kinds become client hooks * **Architecture** - Registry, introspection, config, and contract under the hood * **Error Handling** - Structured errors that flow from server to client ## Why This Matters [#why-this-matters] pyRPC is built for developers who want the safety of a typed backend (Python) with the speed of a modern frontend (TypeScript). By eliminating the "OpenAPI gap", we allow you to focus on your product logic rather than API plumbing. # Mental Model pyRPC treats your backend as a **library of typed functions**. It eliminates the need for heavy build steps by leveraging different strategies for each ecosystem: * **Python**: Uses **dynamic runtime introspection**. Procedures are discovered as you call them. No build step required. * **TypeScript**: Uses **static contract synchronization**. Since TypeScript needs types at development-time for autocompletion, a one-step `codegen` is used to sync the contract. ## Dynamic vs. Static [#dynamic-vs-static] The core of pyRPC is its ability to introspect itself. When you use the Python client, it asks the server "what can you do?" and builds the client on the fly. When you use TypeScript, we use that same introspection power during development to generate a `Types` interface. This gives you the best of both worlds: zero-boilerplate Python and rock-solid TypeScript autocompletion. Every RPC procedure is a Python function decorated with `@rpc`: ```python @rpc def add(a: int, b: int) -> int: return a + b ``` The client calls it the same way: ```python client.add(a=10, b=5) # 15 ``` ## No OpenAPI Middle Layer [#no-openapi-middle-layer] Unlike REST or OpenAPI-based tools, pyRPC does not: * Generate schemas * Require you to maintain `.yaml` or `.json` spec files * Force a separate client codegen step for Python Instead of generating "SDKs", pyRPC synchronizes **typed contracts**. Types flow directly from your procedures; the protocol carries the data, and the client infers the rest. ## End-to-End Typing [#end-to-end-typing] When you add a procedure, the client can call it with full awareness of parameters and return type. Errors are structured and typed too - see [Error Handling](/docs/reference/error-handling). ## Next [#next] * [Architecture](/docs/concepts/architecture) - Deep dive into the Registry, Introspection, and Contract tiers * [Protocol Design](/docs/reference/protocol-design) - How requests and responses are structured * [Error Handling](/docs/reference/error-handling) - How errors are represented and propagated # Procedures A **procedure** is the fundamental unit of pyRPC. It is a plain Python function that you register with a decorator, and it becomes a single, fully-typed, callable endpoint on the client. There is no separate route definition, schema file, or controller, the function *is* the contract. ```python from pyrpc_core import rpc @rpc.query def get_user(user_id: int) -> dict: return {"id": user_id, "name": "Ada"} @rpc.mutation def create_user(name: str) -> dict: return {"id": 1, "name": name} ``` On the client, both are called like local async functions: ```ts const user = await api.get_user({ user_id: 1 }); const created = await api.create_user.mutate({ name: "Ada" }); ``` ## Two kinds: query and mutation [#two-kinds-query-and-mutation] Every procedure is tagged with exactly one **kind**. The kind tells pyRPC (and your frontend adapter) how the procedure should be treated: | Kind | Decorator | Meaning | Client shape | | ------------ | --------------- | ----------------------------------- | -------------------------------- | | **query** | `@rpc.query` | Read-only, safe to retry, cacheable | `useQuery` / `createQuery` | | **mutation** | `@rpc.mutation` | Has side effects, not cached | `useMutation` / `createMutation` | This mirrors the REST/GraphQL distinction between reads and writes, but without any extra configuration. The kind is carried through codegen into the generated `__pyrpc.ts` file, which is how each adapter knows which hook to expose for each procedure. ### Choosing a kind [#choosing-a-kind] * Use **`@rpc.query`** for anything that reads data: `get_user`, `list_items`, `search`. Queries can be prefetched on the server and are safe for TanStack Query's caching, retries, and refetch-on-focus behavior. * Use **`@rpc.mutation`** for anything that changes state: `create_user`, `update_item`, `delete_order`. Mutations never run during prefetch and are the right place for side effects. Tagging a procedure with the wrong kind still works at runtime, but you lose the caching and prefetch semantics that make the client ergonomic. ## The registry [#the-registry] Decorating a function records it in a global **procedure registry** keyed by its name. `mount_fastapi` (or the Flask/Django equivalent) reads that registry to wire up dispatch at `POST /rpc` and schema introspection at `GET /rpc`. For larger apps, group procedures into `Router` objects and merge them into the default router with a prefix: ```python from pyrpc_core import Router from pyrpc_core import default_router users = Router() @users.query def get_user(user_id: int) -> dict: ... default_router.include(users, prefix="users") # → api.users.get_user on the client ``` The prefix becomes part of the client path, so nested routers stay organized without nested URL plumbing. ## How the client sees procedures [#how-the-client-sees-procedures] The generated `Types` interface describes every procedure's input and output. Each adapter turns that description into typed methods: * **React / Next.js**: `api..useQuery()` and `api..useMutation()` (TanStack Query hooks). Next.js adds `api.prefetch.()` for server-side warming. * **Vue**: `pyrpc..createQuery()` / `createMutation()` composables. * **Svelte**: `api..createQuery()` / `createMutation()` stores. * **Vanilla TypeScript / Python**: `client.(...)` direct calls. Because the procedure kind drives the client shape, the same Python decorator decides whether your frontend gets a cacheable query hook or a write mutation. ## Next [#next] * [Mental Model](/docs/concepts/mental-model), how pyRPC thinks about your API * [Error Handling](/docs/reference/error-handling), structured errors that flow back from procedures * [Client: React](/docs/client/adapters/react), turning procedures into hooks in practice # Architecture pyrpc uses [LikeC4](https://likec4.dev) architecture-as-code diagrams to document its structure across multiple levels of detail. The source file is at `architecture/pyrpc.c4`. ## Interactive Diagrams [#interactive-diagrams] ```sh # Start the LikeC4 viewer npx likec4 start architecture ``` This opens an interactive browser at `http://localhost:5173` with all 8 diagrams. ## Static Build [#static-build] ```sh npx likec4 build architecture --output-dir architecture/dist ``` Open `architecture/dist/index.html` to browse the rendered diagrams. ## Diagram Guide [#diagram-guide] ### System Landscape [#system-landscape] The broadest view: Python Developer, TypeScript Developer, JSON-RPC 2.0 Protocol, and pyrpc as a black-box system. ### Container Diagram [#container-diagram] All 7 packages (pyrpc-core, pyrpc-fastapi, pyrpc-flask, pyrpc-django-adapter, pyrpc-codegen, @pyrpc/client, @pyrpc/types) with their relationships. ### Component Diagrams [#component-diagrams] * **pyrpc-core**: Router, Procedure, Interpreter, CLI, ASGI transport (10 components) * **pyrpc-codegen**: TypeScript code generation pipeline (5 components) * **@pyrpc/client**: Proxy dispatch, error handling, CLI sync, postinstall ### Adapter Pattern Comparison [#adapter-pattern-comparison] All four adapters (FastAPI, Flask, Django, ASGI) side by side, showing the "thin shell" pattern. ### Dynamic Views [#dynamic-views] * **RPC Call Flow**: From TypeScript Proxy through HTTP to Procedure execution and back * **Codegen Flow**: From @rpc decorator to TypeScript file output * **Dev Loop**: File watching, hot reload, and auto-regeneration in `pyrpc dev` ## Related Blog Posts [#related-blog-posts] * [Architecture as Code: Mapping pyrpc with LikeC4](/blog/architecture-as-code) * [A Visual Tour of pyrpc's Architecture](/blog/visual-tour) * [Following an RPC Call: From TypeScript to Python and Back](/blog/rpc-call-flow) # Error Handling pyRPC uses structured errors that flow from your procedures to the client. No opaque strings - codes and messages are predictable. ## Server-Side [#server-side] Raise `RPCError` in your procedures: ```python from pyrpc_core import rpc, RPCError @rpc def get_user(user_id: int) -> dict: user = db.find_user(user_id) if not user: raise RPCError(-404, "User not found") return user ``` ## Client-Side (TypeScript) [#client-side-typescript] Catch `PyRPCError` when calling procedures. The client ensures that server-side errors are caught and surfaced with their original codes and messages. ```ts import { PyRPCError } from "@pyrpc/client"; try { const user = await client.get_user(999); } catch (e) { if (e instanceof PyRPCError) { console.error(e.code); // -404 console.error(e.message); // User not found } } ``` ## Standard Codes [#standard-codes] | Code | Meaning | | ------ | ---------------- | | -32600 | Invalid Request | | -32601 | Method not found | | -32602 | Invalid params | | -32603 | Internal error | Use negative codes (e.g. -404, -401) for application-specific errors. ## Next [#next] * [Server](/docs/server) - Routers, context, authorization * [Client](/docs/client) - Sync, async, and TypeScript codegen # FAQ ## What is pyRPC? [#what-is-pyrpc] pyRPC is a type-safe RPC framework for Python backends with TypeScript frontends. Define procedures with `@rpc`, get TypeScript types automatically, no OpenAPI, no codegen pipelines. ## How is this different from tRPC? [#how-is-this-different-from-trpc] tRPC is TypeScript-only. pyRPC brings the same end-to-end typing to Python + TypeScript stacks. ## How is this different from OpenAPI? [#how-is-this-different-from-openapi] OpenAPI is a specification file that must be manually kept in sync with your implementation. pyRPC treats your Python functions as the source of truth, types are derived directly from your code, not from a separate spec. ## Does pyRPC support async procedures? [#does-pyrpc-support-async-procedures] Yes. The interpreter detects whether your function is `async def` or `def` and dispatches accordingly. The TypeScript client uses schema introspection to determine whether to `await` a call. ## Which frameworks are supported? [#which-frameworks-are-supported] FastAPI, Flask, and Django. See [Adapters](/docs/server/adapters) for details. ## Can I use pyRPC without a framework? [#can-i-use-pyrpc-without-a-framework] Yes. The standalone ASGI adapter (`PyRPCAsgiApp`) can be deployed directly without FastAPI, Flask, or Django. ## Do I need to install anything besides pyrpc-core? [#do-i-need-to-install-anything-besides-pyrpc-core] Install `pyrpc-core` for the runtime, CLI, and codegen. Add framework adapters via extras: `pyrpc-core[fastapi]`, `pyrpc-core[flask]`, `pyrpc-core[django]`. ## How do I generate TypeScript types? [#how-do-i-generate-typescript-types] Run `pyrpc dev`. On first run it asks for your backend framework and entry point plus your client setup, creates a `pyrpc.json` config, and generates `__pyrpc.ts` in your client project automatically. For CI, use `pyrpc codegen`. ## How do TypeScript types reach my client? [#how-do-typescript-types-reach-my-client] `pyrpc dev` writes `__pyrpc.ts` to your client project root and wires `@pyrpc/types` to it: a tsconfig `paths` alias plus a bundler alias for Vite, SvelteKit, and Next.js Turbopack. The types regenerate automatically whenever you save a `.py` file. ## Is pyRPC production-ready? [#is-pyrpc-production-ready] pyRPC is in active development. APIs may change. Follow the [changelog](/changelog) and [roadmap](https://github.com/pyrpc/pyrpc/blob/main/ROADMAP.md) for direction. # Further Reading ## Blog Posts [#blog-posts] ### Getting Started [#getting-started] * [Why pyRPC?](/blog/why-pyrpc), The philosophy and motivation behind the project * [Building a full-stack app with pyRPC](/blog/building-a-full-stack-app-with-pyrpc), Step-by-step tutorial * [From raw FastAPI to pyRPC](/blog/from-raw-fastapi-to-pyrpc), Migration guide ### Deep Dives [#deep-dives] * [Architecture as Code: Mapping pyrpc with LikeC4](/blog/architecture-as-code), Interactive architecture diagrams with likec4 * [A Visual Tour of pyrpc's Architecture](/blog/visual-tour), Guided walkthrough of all 8 diagrams * [Following an RPC Call: From TypeScript to Python and Back](/blog/rpc-call-flow), End-to-end trace through the call flow * [Dev console architecture](/blog/dev-console-architecture), How `pyrpc dev` works under the hood * [Distribution modes: workspace and server](/blog/distribution-modes), Type distribution strategies * [Three deployment architectures](/blog/three-deployment-architectures), Monorepo, separate repos, and published packages * [Path resolution: config-relative, not CWD-relative](/blog/path-resolution-config-relative), Why relative paths are rejected * [Windows compatibility in a Python OSS project](/blog/windows-compatibility-in-python-oss), Cross-platform lessons ### Release Notes [#release-notes] * [v0.7.3, Django adapter, FastAPI/Flask fixes](/blog/v0-7-3-django-adapter) * [v0.6.0, Client distribution and package standardization](/blog/v0-6-0-release) * [v0.3.3, Cleaner types, no more /rpc/rpc, CORS](/blog/v0-3-3-client-and-watcher-fixes) * [v0.3.0, pyrpc-cli merged into core, one-command install](/blog/v0-3-0-single-install) ## External Resources [#external-resources] * [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification) * [tRPC Documentation](https://trpc.io/docs), The TypeScript inspiration for pyRPC * [Pydantic v2 Documentation](https://docs.pydantic.dev/latest/), Runtime type validation used by pyRPC # Protocol Design pyRPC uses **JSON-RPC 2.0** over HTTP POST. One endpoint, one format, no surprises. ## Request [#request] ``` POST /rpc Content-Type: application/json ``` ```json { "id": "unique-request-id", "method": "add", "params": { "a": 10, "b": 5 } } ``` * `id` - Unique identifier for the request (UUID or string) * `method` - Procedure name * `params` - Object (named) or array (positional) of arguments ## Response [#response] **Success:** ```json { "id": "unique-request-id", "result": 15 } ``` **Error:** ```json { "id": "unique-request-id", "error": { "code": -32602, "message": "Invalid params" } } ``` ## Introspection [#introspection] One of pyRPC's most powerful features is built-in introspection. By default, your RPC endpoint also responds to `GET` requests: ``` GET /rpc ``` The server returns a full JSON schema of every registered procedure, including: * Parameter names and types * Return types * Docstrings * Namespaces This is the "Source of Truth" that `pyrpc codegen` uses to synchronize your TypeScript contracts. ## Why JSON-RPC? [#why-json-rpc] * **Zero Ambiguity** - Unlike REST, where status codes (200, 201, 204) and methods (PUT vs PATCH) are often debated, JSON-RPC has one way to succeed and one way to fail. * **Easy Debugging** - Requests are plain JSON objects. You can copy-paste them from your browser's network tab directly into a test script. * **Batched Requests** - The protocol natively supports sending multiple calls in a single HTTP request (coming soon to pyRPC). ## Next [#next] * [Error Handling](/docs/reference/error-handling) - Error codes and propagation * [Server](/docs/server) - Mounting, procedures, context # Python Client ## RPCClient [#rpcclient] ```python from pyrpc_core import RPCClient client = RPCClient("http://localhost:8000") ``` A dynamic RPC client that lets you call remote procedures as if they were local methods. ### Constructor [#constructor] ```python RPCClient(base_url, async_client=None, sync_client=None) ``` | Parameter | Type | Description | | -------------- | --------------------------- | ----------------------------------------------------------- | | `base_url` | `str` | Base URL of the pyRPC server (e.g. `http://localhost:8000`) | | `async_client` | `httpx.AsyncClient \| None` | Optional custom async HTTP client | | `sync_client` | `httpx.Client \| None` | Optional custom sync HTTP client | ### Dynamic Dispatch [#dynamic-dispatch] ```python with RPCClient("http://localhost:8000") as client: result = client.add(a=10, b=5) # sync if procedure is sync, async if async print(result) # 15 ``` The client fetches the procedure schema via `GET /rpc` on first call to determine whether a procedure is sync or async. If the schema is unavailable, it falls back to running event-loop detection. ### `call_sync(method, *args, **kwargs)` [#call_syncmethod-args-kwargs] Explicit synchronous call: ```python result = client.call_sync("add", a=10, b=5) ``` ### `call_async(method, *args, **kwargs)` [#call_asyncmethod-args-kwargs] Explicit asynchronous call: ```python result = await client.call_async("add", a=10, b=5) ``` ### `.aio()` [#aio] Force async dispatch on a specific call: ```python result = await client.add.aio(a=10, b=5) ``` ### `set_schema(schema)` [#set_schemaschema] Manually provide the procedure schema, bypassing the HTTP introspection fetch: ```python client.set_schema({"add": False, "greet": True}) ``` ### Context Manager [#context-manager] ```python # Sync with RPCClient("http://localhost:8000") as client: ... # Async async with RPCClient("http://localhost:8000") as client: ... ``` *** ## RPCError [#rpcerror] ```python from pyrpc_core import RPCError ``` Raised when the server returns a JSON-RPC error response. | Attribute | Type | Description | | --------- | ----- | ------------------- | | `code` | `int` | JSON-RPC error code | | `message` | `str` | Error message | # Core ## Router [#router] ```python from pyrpc_core import Router router = Router() ``` The `Router` manages a set of RPC procedures. It can be used as a decorator and merged with other routers. ### `router.rpc(name=None)` [#routerrpcnamenone] Decorator to register a function as an RPC procedure. ```python @router.rpc def add(a: int, b: int) -> int: return a + b @router.rpc(name="custom_name") def my_func(): ... ``` ### `router.register(name, proc)` [#routerregistername-proc] Register a `Procedure` instance with an explicit name. ### `router.merge(other, prefix="")` [#routermergeother-prefix] Merge another router's procedures into this one, optionally with a name prefix. ```python router.merge(sub_router, prefix="admin_") ``` ### `router.get(name)` [#routergetname] Look up a procedure by name. Returns `Procedure` or `None`. ### `router.list()` [#routerlist] Return a list of all registered procedure names. ### `router.reload_module(module_path)` [#routerreload_modulemodule_path] Reload a Python module and atomically replace this router's procedures. Returns `True` if the router was updated. *** ## Global Decorators [#global-decorators] ```python from pyrpc_core import rpc, model, default_router ``` * `rpc`: Alias for `default_router.rpc`. Registers in the global default router. * `model`: Alias for `pydantic.dataclasses.dataclass`. Use to define structured parameter types. * `default_router`: A module-level `Router()` singleton used by the `rpc` decorator. *** ## handle\_request [#handle_request] ```python from pyrpc_core import handle_request await handle_request(payload, router=None) ``` Parse and dispatch an incoming JSON-RPC request. Returns a JSON-RPC response dict. A **batch** payload (a JSON list of operation objects) is dispatched operation-by-operation through the same router, sequentially, and returns one response per operation in the same order. Batches are not transactions, each operation keeps its own result or error. | Parameter | Type | Description | | --------- | ---------------------------------------- | -------------------------------------------------------- | | `payload` | `Dict[str, Any] \| List[Dict[str, Any]]` | The parsed JSON-RPC request body (single or batch) | | `router` | `Router \| None` | Router to dispatch against; defaults to `default_router` | Error codes: `-32600` (invalid request), `-32601` (method not found), `-32602` (invalid params), `-32603` (internal error). *** ## get\_registry\_schema [#get_registry_schema] ```python from pyrpc_core import get_registry_schema schemas = get_registry_schema(router) ``` Generate a dict of `ProcedureSchema` objects for all procedures in a router. Used by the `GET /rpc` introspection endpoint. Returns `Dict[str, ProcedureSchema]`. *** ## get\_procedure\_schema [#get_procedure_schema] ```python from pyrpc_core import get_procedure_schema schema = get_procedure_schema(procedure) ``` Generate a `ProcedureSchema` from a compiled `Procedure` instance. *** ## PyRPCAsgiApp [#pyrpcasgiapp] ```python from pyrpc_core import PyRPCAsgiApp, asgi_app app = PyRPCAsgiApp(router=None) app = asgi_app # pre-configured instance with default_router ``` Standalone ASGI transport. Handles `POST /rpc` and `GET /rpc` with CORS headers. ```python import uvicorn uvicorn.run(asgi_app) ``` *** ## RpcRequest / RpcResponse [#rpcrequest--rpcresponse] ```python from pyrpc_core import RpcRequest, RpcResponse ``` Pydantic models for JSON-RPC request and response envelopes. Used internally by `handle_request`. *** ## ProcedureSchema [#procedureschema] ```python class ProcedureSchema(BaseModel): name: str parameters: List[ParameterSchema] return_type: str return_schema: Dict[str, Any] doc: Optional[str] = None is_async: bool = False ``` Schema describing a registered procedure. Returned by `get_registry_schema`. Each `ParameterSchema` has: `name`, `type`, `schema_` (JSON schema), `required`, `default`. # TypeScript Client ## createClient [#createclient] ```typescript import { createClient, httpBatchLink } from "@pyrpc/client" import type { Types } from "@pyrpc/types" const client = createClient({ links: [ httpBatchLink({ url: "https://api.example.com", }), ], }) ``` Creates a typed proxy that maps each procedure name to an async function. All procedure calls return `Promise`. ### Options [#options] ```typescript interface ClientOptions { links: Link[] } ``` | Option | Description | | ------- | ----------------------------------------------------------------------------------------------------------------------- | | `links` | Link pipeline. Exactly one terminating link (`httpLink` or `httpBatchLink`); zero or multiple is a configuration error. | ## httpLink [#httplink] `httpLink` sends one RPC operation per HTTP request. ```typescript httpLink({ url: "https://api.example.com" }) ``` | Option | Description | | ------ | ------------------------------------------ | | `url` | Server URL. The link normalizes to `/rpc`. | ## httpBatchLink [#httpbatchlink] `httpBatchLink` combines multiple independent RPC operations that occur close together into one HTTP request. ```typescript httpBatchLink({ url: "https://api.example.com", maxItems: 10 }) ``` | Option | Description | | ---------- | ----------------------------------------------------------------------------- | | `url` | Server URL. The link normalizes to `/rpc`. | | `maxItems` | Max operations per batch; flushed early when reached. Defaults to `Infinity`. | Batched operations execute sequentially on the server and each keeps its own result or error. A batch is not a transaction. ### Usage [#usage] ```typescript const client = createClient({ links: [ httpBatchLink({ url: "https://api.example.com", }), ], }) // Fully typed, parameters and return type inferred from the server const result = await client.add(10, 5) ``` # Spec pyRPC uses **JSON-RPC 2.0** over HTTP. See [Protocol Design](/docs/reference/protocol-design) for a detailed explanation. ## Endpoints [#endpoints] | Method | Path | Description | | ------ | ------ | ---------------------------------------------------------- | | `POST` | `/rpc` | Dispatch an RPC method call | | `GET` | `/rpc` | Introspection, returns schema of all registered procedures | ## Error Codes [#error-codes] | Code | Message | Description | | -------- | ---------------- | ---------------------------------------------- | | `-32600` | Invalid Request | Malformed JSON-RPC envelope | | `-32601` | Method not found | Procedure name not registered in router | | `-32602` | Invalid params | Type validation failure on arguments | | `-32603` | Internal error | Unhandled exception during procedure execution | ## Transport [#transport] * HTTP/1.1 or HTTP/2 * Content-Type: `application/json` * The standalone ASGI transport adds CORS headers (all origins) * Framework adapters defer CORS to the host application # Context Procedures receive only the JSON-RPC params. pyRPC's core does not pass HTTP request context (headers, user, etc.) into procedures directly. ## Adapter-Specific Context [#adapter-specific-context] If you need request context, use your adapter's features: **FastAPI** - Use `Depends` to inject request-scoped data. Your RPC endpoint is a FastAPI route, so you can depend on `Request`, auth, or custom dependencies. How this integrates with pyRPC's handler depends on the adapter implementation. **Flask** - Use `flask.g` or `flask.request` inside procedures. Ensure your procedure runs in a request context (it will when invoked via the Flask-mounted endpoint). ## Example (Flask) [#example-flask] ```python from flask import request, g from pyrpc_core import rpc @rpc def get_current_ip() -> str: return request.remote_addr ``` When the procedure is called via the Flask RPC endpoint, `request` is in scope. ## Future Support [#future-support] Planned improvements may add explicit context injection (e.g. a `Context` parameter) so procedures can access request data in an adapter-agnostic way. # Middleware pyRPC is mounted as a route on your framework. Use the framework's middleware to wrap requests before they reach the RPC handler. ## FastAPI [#fastapi] Add middleware to the FastAPI app. It runs for all routes, including the RPC endpoint: ```python from fastapi import FastAPI from pyrpc_core import rpc from pyrpc_fastapi import mount_fastapi app = FastAPI() @app.middleware("http") async def log_requests(request, call_next): # Log, modify headers, etc. response = await call_next(request) return response @rpc def add(a: int, b: int) -> int: return a + b mount_fastapi(app) ``` ## Flask [#flask] Use Flask's `before_request`, `after_request`, or extensions like Flask-CORS: ```python from flask import Flask from pyrpc_core import rpc from pyrpc_flask import mount_flask app = Flask(__name__) @app.before_request def before(): # Auth, logging, etc. pass @rpc def add(a: int, b: int) -> int: return a + b mount_flask(app) ``` ## ASGI [#asgi] For the standalone ASGI app, wrap it with ASGI middleware: ```python from pyrpc_core import asgi_app # Wrap with your ASGI middleware app = YourMiddleware(asgi_app) ``` # Overview # Server [#server] This section covers everything you need to build a pyRPC server. ## What You'll Learn [#what-youll-learn] * **[Routers](/docs/server/routers)** - The procedure registry, `@rpc`, and organizing procedures * **[Procedures](/docs/server/procedures)** - Defining procedures, types, sync vs async * **[Context](/docs/server/context)** - Accessing request and adapter context * **[Middleware](/docs/server/middleware)** - Framework middleware around your RPC endpoint * **[Adapters](/docs/server/adapters)** - FastAPI, Flask, Django, and ASGI integration ## Quick Reference [#quick-reference] ```python from fastapi import FastAPI from pyrpc_core import rpc from pyrpc_fastapi import mount_fastapi app = FastAPI() @rpc def add(a: int, b: int) -> int: return a + b mount_fastapi(app) ``` That's the core pattern: register with `@rpc`, mount with an adapter. # Procedures Procedures are Python functions decorated with `@rpc`. They receive validated parameters from the client and return a result. ## Basic Definition [#basic-definition] ```python from pyrpc_core import rpc @rpc def add(a: int, b: int) -> int: return a + b ``` ## Query vs mutation [#query-vs-mutation] Procedures can declare a **kind** so framework adapters expose the right TanStack Query hook: ```python from pyrpc_core import rpc @rpc.query def get_user(user_id: int) -> dict: return {"id": user_id} @rpc.mutation def update_user(user_id: int, name: str) -> dict: return {"id": user_id, "name": name} ``` * `@rpc` and `@rpc.query` → kind `query` (React/Vue: `useQuery`, Svelte: `createQuery`) * `@rpc.mutation` → kind `mutation` (`useMutation` / `createMutation`) Codegen emits `ProcedureKinds` and `procedureKinds` in `@pyrpc/types` for the adapters. ### The @model Decorator [#the-model-decorator] The `@model` decorator is the preferred way to define complex data structures in pyRPC. It is a thin wrapper around Pydantic dataclasses that ensures your types are perfectly captured during introspection. ```python from pyrpc_core import model @model class User: id: int name: str @rpc def get_user(id: int) -> User: return User(id=id, name="Paul Graham") ``` When you run `pyrpc codegen`, this class is automatically synchronized as a TypeScript `interface`. ### Universal Pydantic Validation [#universal-pydantic-validation] pyRPC uses Pydantic's `TypeAdapter` to automatically validate **every** parameter and return type. This means you get production-grade validation for free. ### Return Type Validation [#return-type-validation] If you specify a return type hint, pyRPC validates the result **before** sending it to the client. If your function returns the wrong data type, pyRPC will raise an Internal Error (-32603) instead of sending invalid data to the client. ## Sync vs Async [#sync-vs-async] Procedures can be synchronous or asynchronous. pyRPC automatically detects and awaits coroutines. ```python @rpc async def get_data_async(id: str): await asyncio.sleep(1) return {"id": id} ``` ## Error Handling [#error-handling] If validation fails, pyRPC returns a standard JSON-RPC `Invalid Params` error (-32602) with a detailed `data` field explaining exactly which field failed and why. ```json { "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Validation failed", "data": { "field": "users.0.id", "message": "Input should be a valid integer", "type": "int_parsing" } } } ``` ## Next Steps [#next-steps] * [Routers](/docs/server/routers) - Organize your procedures into modules * [Adapters](/docs/server/adapters) - FastAPI, Flask, Django, and ASGI # Routers pyRPC allows you to organize your API into modular **Routers**. This is essential for large applications where you want to split procedures across different files and namespaces. ## The Default Router [#the-default-router] For simple applications, you can use the global `rpc` decorator. All procedures registered this way go into the `default_router`. ```python from pyrpc_core import rpc from pyrpc_fastapi import mount_fastapi @rpc def add(a: int, b: int) -> int: return a + b # Mounts the default_router at /rpc mount_fastapi(app) ``` ## Modular Routers [#modular-routers] As your app grows, you can create isolated `Router` instances. ```python from pyrpc_core import Router # Create a scoped router user_router = Router() @user_router.rpc def get_profile(user_id: int): return {"id": user_id, "name": "Alice"} ``` ## Merging Routers [#merging-routers] You can merge multiple routers into a single "root" router. This supports **prefixes**, which is a powerful way to namespace your API. ```python from pyrpc_core import Router from pyrpc_fastapi import mount_fastapi # Sub-routers auth_router = Router() @auth_router.rpc def login(): ... post_router = Router() @post_router.rpc def create(): ... # Main app router app_router = Router() app_router.merge(auth_router, prefix="auth.") app_router.merge(post_router, prefix="post.") # Mount the specific router mount_fastapi(app, app_router) ``` In this example: * The login procedure is available as `auth.login` * The create procedure is available as `post.create` ## Custom Procedure Names [#custom-procedure-names] You can still override individual procedure names within a router: ```python @user_router.rpc(name="get_user") def fetch_user_by_id(user_id: int): ... ``` ## Next Steps [#next-steps] * [Procedures](/docs/server/procedures) - Automatic Pydantic validation and types * [Adapters](/docs/server/adapters) - FastAPI, Flask, Django, and ASGI * [Core Reference](/docs/reference/prpc-core) - Router, procedure, and introspection APIs # Code Generation # pyrpc-codegen [#pyrpc-codegen] Automatically generate a TypeScript types interface from your pyRPC server definition. ## Installation [#installation] ```bash # One install - runtime, CLI, and codegen all included pip install pyrpc-core ``` > `pyrpc-codegen` is an internal dependency of `pyrpc-core`. You only install it separately if you need to call the codegen API programmatically without the runtime (e.g., a CI script). Most users never need this. ## Quick Setup [#quick-setup] Run `pyrpc dev` from your Python project root: ```bash pyrpc dev ``` On first run, it will prompt for your backend framework and entry point, then your client project root and frontend framework. This creates a `pyrpc.json` config file, generates `__pyrpc.ts` in your client project, and wires `@pyrpc/types` to it automatically. ### Configuration file (`pyrpc.json`) [#configuration-file-pyrpcjson] ```json { "backend": { "framework": "fastapi", "entrypoint": "main:app" }, "clients": [ { "framework": "Next.js", "root": "../frontend" } ] } ``` * `backend.framework` - `fastapi`, `flask`, `django`, or `asgi`; decides the native dev server (`uvicorn`, `flask run`, or `manage.py runserver`). * `backend.entrypoint` - framework-specific: a `module[:app]` target 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`, required for Django). * `clients[].root` - one or more frontend project roots; each gets its own generated types. All paths in `pyrpc.json` are resolved relative to the config file's directory. The types output is `/__pyrpc.ts`, derived from each client root automatically. ## CLI Usage [#cli-usage] The `pyrpc` CLI (included with `pyrpc-core`) provides these subcommands: | Command | Description | | --------- | ----------------------------------------------------------------------- | | `dev` | Start dev server with auto-type regeneration and interactive console | | `watch` | Watch for Python changes and regenerate types (no server started) | | `pull` | Extract RPC schema from a Python module and save as JSON | | `codegen` | Generate TypeScript types from a schema file, running server, or module | | `serve` | Start the pyRPC ASGI server | | `inspect` | List all registered RPC procedures in a module | | `version` | Show pyRPC version | ### `pyrpc dev` [#pyrpc-dev] Start the development server with automatic type regeneration and interactive console. ```bash pyrpc dev ``` Flags: | Flag | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `--yes`, `-y` | Skip the setup wizard; sniff the framework and auto-detect module and client (errors if the framework can't be detected) | | `--framework`, `-f` | Backend framework: `fastapi`, `flask`, `django`, or `asgi` (requires `--yes`) | | `--module`, `-m` | Entry point - `module[:app]` for FastAPI/Flask/ASGI, types module for Django (requires `--yes`) | | `--client`, `-c` | Client project root (requires `--yes`) | | `--reconfigure` | Re-run the setup wizard even if `pyrpc.json` exists | | `--reload/--no-reload` | Dev-server auto-reload (default: on) | | `--host`, `-h` | Bind socket to this host (default: 127.0.0.1) | | `--port`, `-p` | Bind socket to this port (default: 8000) | ### `pyrpc watch` [#pyrpc-watch] Regenerate types on Python changes without starting a server. Reads `pyrpc.json` by default: ```bash pyrpc watch pyrpc watch --client ../frontend ``` ### `pyrpc pull` [#pyrpc-pull] Extract the RPC schema from your Python module into a JSON file: ```bash pyrpc pull my_app.main -o pyrpc-schema.json ``` This is useful for CI workflows - commit the schema file to your repo so frontend builds can generate types without a running Python server: ```bash git add pyrpc-schema.json git commit -m "chore(codegen): update RPC schema" ``` ### `pyrpc codegen` [#pyrpc-codegen-1] Generate TypeScript types into `/__pyrpc.ts`: ```bash # From a schema file pyrpc codegen pyrpc-schema.json --client ../frontend # Directly from a running server (no `pull` required) pyrpc codegen http://localhost:8000 --client ../frontend # Or from a Python module pyrpc codegen my_app.main --client ../frontend ``` `--client` defaults to `.`. ### Full Workflow (Manual) [#full-workflow-manual] ```bash # Install everything pip install pyrpc-core # Or use the dev server pyrpc dev # Manual codegen pyrpc codegen http://localhost:8000 --client ../frontend ``` ## Features [#features] * TypeScript types interface generation * Full type inference (int ↔ number, str ↔ string, etc.) * Documentation comments preserved * Auto-regeneration on file change (when using `pyrpc dev`) * **Runtime kind map** — `procedureKinds` is emitted alongside the types, so framework adapters know which TanStack hook (`useQuery` vs `useMutation`) each procedure exposes * Pure-Python generator (Jinja2 + `jsonschema-ts`), no Node.js dependency ## Generated Output [#generated-output] `pyrpc dev` (or `pyrpc codegen`) writes `/__pyrpc.ts`: ```typescript // /__pyrpc.ts export type Types = { add(a: number, b: number): Promise; greet(name: string): Promise; } ``` Use it with `@pyrpc/client`: ```typescript import { createClient, httpBatchLink } from "@pyrpc/client" import type { Types } from "@pyrpc/types" const client = createClient({ links: [ httpBatchLink({ url: "http://localhost:8000", }), ], }) const result = await client.add(10, 5) // Fully typed, with autocompletion! ``` `@pyrpc/types` resolves to the generated file via a tsconfig path alias (`"paths": { "@pyrpc/types": ["./__pyrpc.ts"] }`) and a bundler alias for Vite/SvelteKit/Next.js Turbopack, both injected by `pyrpc dev`. ## Next Steps [#next-steps] * Learn about [Client Usage](/docs/client) * Explore [TypeScript Integration](/docs/client/vanilla) # HTTP Batch Link `httpBatchLink` is a terminating link that automatically combines **multiple independent RPC operations that occur close together into a single HTTP request**. ``` multiple RPC operations ↓ one HTTP request ``` You still write normal, independent RPC calls. The link collects eligible operations from the same scheduling window and sends them as one batch: ```ts import { createClient, httpBatchLink } from "@pyrpc/client" import type { Types } from "@pyrpc/types" export const client = createClient({ links: [ httpBatchLink({ url: "https://api.example.com", maxItems: 10, }), ], }); // The three concurrent calls are combined into one HTTP request. const [alice, bob, carol] = await Promise.all([ client.get_user({ id: 1 }), client.get_user({ id: 2 }), client.get_user({ id: 3 }), ]); ``` ## How It Works [#how-it-works] Operations that occur in the same scheduling window are collected and sent as a single JSON array to the server's `/rpc` endpoint: ``` POST /rpc Content-Type: application/json [ {"id":"a1b2c3","method":"get_user","params":{"id":1}}, {"id":"d4e5f6","method":"get_user","params":{"id":2}} ] ``` The server responds with an array of results, matched back to the pending calls by index. Each operation keeps its own `id` and resolves or rejects independently — so one failed procedure never fails the others. The HTTP request succeeding is a transport success; a per-operation error is an operation failure that surfaces as a [`PyRPCError`](/docs/reference/error-handling) on that call only. Only a failed HTTP request fails the whole batch. ## Batching Semantics [#batching-semantics] * Both queries and mutations can be batched. pyRPC uses `POST` for both, so they can share one batch. * Operations inside a batch execute **sequentially on the server**, in request order. * A batch is **not a transaction**: if one operation fails, the others still execute, and successful operations are not rolled back. * Each operation keeps its own result or error. A single failed procedure does not fail the others. ## Options [#options] * `url`: server URL (required). Give the root (`https://api.example.com`) or the full endpoint (`https://api.example.com/rpc`); the link normalizes to `/rpc`. * `maxItems`: maximum operations per batch; when more operations are queued, they flush immediately rather than waiting for the batching window. Defaults to `Infinity`. # HTTP Link `httpLink` is a terminating link that sends **one RPC operation per HTTP request**. ``` one RPC operation ↓ one HTTP request ``` ```ts import { createClient, httpLink } from "@pyrpc/client" import type { Types } from "@pyrpc/types" export const client = createClient({ links: [ httpLink({ url: "https://api.example.com", }), ], }); const user = await client.get_user({ id: 1 }); ``` ## How It Works [#how-it-works] Each call POSTs the operation as JSON to the server's `/rpc` endpoint with `Content-Type: application/json`: ``` POST /rpc Content-Type: application/json {"id":"k3j2h1","method":"get_user","params":{"id":1}} ``` The response is the wire result of the single operation: ```json { "id": "k3j2h1", "result": { "id": 1, "name": "Ada" }, "error": null } ``` If the procedure fails on the server, `result` is `null` and `error` carries the code and message — the client throws a [`PyRPCError`](/docs/reference/error-handling) from it. If the HTTP request itself fails (non-2xx status), the link throws an HTTP error before any result parsing happens. For day-to-day use, reach for [`httpBatchLink`](/docs/client/links/http-batch-link) instead; it sends the same calls with fewer round trips. ## Options [#options] * `url`: server URL (required). Give the root (`https://api.example.com`) or the full endpoint (`https://api.example.com/rpc`); the link normalizes to `/rpc`. # Overview # Links [#links] A **link** transports an RPC operation to the server. The URL, HTTP transport, request serialization, response deserialization, and HTTP-level error handling all live in the link — the client core only knows how to turn procedure calls into operations and results back into typed values. ## Configuration [#configuration] Links are configured on the client. The URL belongs to the link, not the client: ```ts import { createClient, httpBatchLink } from "@pyrpc/client" import type { Types } from "@pyrpc/types" export const client = createClient({ links: [ httpBatchLink({ url: "https://api.example.com", }), ], }); ``` Exactly one terminating link is supported (`httpLink` or `httpBatchLink`); supplying zero or multiple terminating links is a configuration error. ## Available Links [#available-links] | Link | Behavior | Use when | | ----------------------------------------------------- | ------------------------------------------------------ | ------------------------------ | | [`httpLink`](/docs/client/links/http-link) | One RPC operation → one HTTP request | Debugging, low-traffic clients | | [`httpBatchLink`](/docs/client/links/http-batch-link) | Multiple independent RPC operations → one HTTP request | Day-to-day use | For day-to-day use, reach for `httpBatchLink`; it sends the same calls with fewer round trips. ## The Operation Wire Format [#the-operation-wire-format] Every call is transported as an **operation** — "run procedure `method` with `params`": ```json { "id": "k3j2h1", "method": "get_user", "params": { "id": 1 } } ``` The link POSTs the operation (or an array of operations) to the server and resolves each call with its result. If the HTTP request succeeds but the procedure itself fails, the `error` field is set and the client throws a `PyRPCError` — the two are mutually exclusive. Non-terminating links (logging, retry, auth, splitting) are not implemented yet; the interface is kept minimal so they can be added later without changing the client core. ## Next Steps [#next-steps] * [HTTP Link](/docs/client/links/http-link) — one operation per request * [HTTP Batch Link](/docs/client/links/http-batch-link) — batch concurrent operations # Next.js `@pyrpc/next` extends `@pyrpc/react` with React Server Component helpers: `api.prefetch`, `api.dehydrate()`, and `api.HydrationBoundary`. Server components warm the TanStack cache; client components call `useQuery` and get instant data with no loading flicker. ## Installation [#installation] ```bash npm install @pyrpc/next @tanstack/react-query ``` ## Project structure [#project-structure] ``` my-app/ app/ layout.tsx ← RootLayout with page.tsx ← Server component: prefetch counter.tsx ← Client component: useQuery/useMutation providers.tsx ← "use client" QueryClient + api.Provider lib/ pyrpc.ts ← createNextClient, import this everywhere ``` ## 1. Create the client [#1-create-the-client] ```ts title="lib/pyrpc.ts" import { createNextClient, httpBatchLink } from "@pyrpc/next" import type { Types } from "@pyrpc/types" export const api = createNextClient({ links: [ httpBatchLink({ url: process.env.PYRPC_URL ?? "http://localhost:8000", }), ], }) ``` One file, one import everywhere, same variable on the server and in client components. ## 2. Set up Providers [#2-set-up-providers] ```tsx title="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 ( {children} ) } ``` ```tsx title="app/layout.tsx" import { Providers } from "./providers" export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ## 3. Server component: prefetch [#3-server-component-prefetch] Prefetch in a Server Component so the client gets data on first render with no loading state: ```tsx title="app/page.tsx" import { api } from "@/lib/pyrpc" import { Counter } from "./counter" export default async function Page() { // warm the cache server-side await api.prefetch.read_root() await api.prefetch.read_item({ item_id: 42, q: "test" }) return ( ) } ``` ## 4. Client component: hooks [#4-client-component-hooks] ```tsx title="app/counter.tsx" "use client" import { api } from "@/lib/pyrpc" import { useState } from "react" export function Counter() { const [name, setName] = useState("") // data is already in cache, no loading flicker const { data: greeting } = api.read_root.useQuery() const { data: item } = api.read_item.useQuery({ item_id: 42, q: "test" }) const createItem = api.create_item.useMutation() return (
{JSON.stringify(greeting)}
{JSON.stringify(item)}
setName(e.target.value)} /> {createItem.isSuccess &&
{JSON.stringify(createItem.data)}
}
) } ``` ## What's on `api` [#whats-on-api] | Property | Where | Description | | --------------------------- | --------------- | ------------------------------------------ | | `api..useQuery(…)` | Client | TanStack `useQuery` hook | | `api..useMutation(…)` | Client | TanStack `useMutation` hook | | `api.Provider` | Client | Provides the TanStack cache | | `api.useUtils()` | Client | Cache utilities (invalidate, setData…) | | `api.prefetch.(…)` | Server | Warms the cache before rendering | | `api.dehydrate()` | Server | Serializes the cache for handoff | | `api.HydrationBoundary` | Server → Client | Passes dehydrated cache to the browser | | `api.createCaller()` | Server | Direct Promise calls (queries + mutations) | ## Skipping prefetch [#skipping-prefetch] Prefetch is optional. If you skip it, the client component fetches on mount exactly like plain `@pyrpc/react`: ```tsx title="app/page.tsx" // no prefetch export default function Page() { return } ``` ## Server-side mutations with createCaller [#server-side-mutations-with-createcaller] Mutations can't be prefetched (they have side effects), but you can call them from Server Actions or Route Handlers: ```ts title="app/actions.ts" "use server" import { api } from "@/lib/pyrpc" export async function serverCreate(name: string) { const caller = api.createCaller() return caller.create_item({ name }) } ``` ## Full working examples [#full-working-examples] | Server | Source | | ------- | --------------------------------------------------------------------------------------------- | | FastAPI | [`examples/fastapi-nextjs`](https://github.com/pyrpc/pyrpc/tree/main/examples/fastapi-nextjs) | | Flask | [`examples/flask-nextjs`](https://github.com/pyrpc/pyrpc/tree/main/examples/flask-nextjs) | | Django | [`examples/django-nextjs`](https://github.com/pyrpc/pyrpc/tree/main/examples/django-nextjs) | # React `@pyrpc/react` wraps your pyRPC procedures in TanStack Query hooks. Every procedure becomes a fully typed `useQuery` or `useMutation` -- no hand-written types, no schema files. ## Installation [#installation] ```bash npm install @pyrpc/react @tanstack/react-query ``` That's the only install you need. `@pyrpc/types` and `@pyrpc/client` ship as dependencies of every adapter, and `pyrpc dev` generates your types into `__pyrpc.ts` and wires the `@pyrpc/types` import to it automatically. ## Project structure [#project-structure] ``` my-app/ src/ lib/ pyrpc.ts <- create the client once here index.tsx <- wrap root in api.Provider App.tsx <- call api.greet.useQuery() etc. __pyrpc.ts <- written by pyrpc dev (do not edit) ``` ## 1. Create the client [#1-create-the-client] ```ts title="src/lib/pyrpc.ts" import { createReactClient, httpBatchLink } from "@pyrpc/react" import type { Types } from "@pyrpc/types" export const api = createReactClient({ links: [ httpBatchLink({ url: process.env.REACT_APP_API_URL ?? "http://localhost:8000", }), ], }) ``` `api` is a single object. It carries every procedure as a hook, plus `api.Provider` for the TanStack Query cache. ## 2. Add the Provider [#2-add-the-provider] Wrap your root component once -- this sets up the shared TanStack Query cache: ```tsx title="src/index.tsx" import React from "react" import ReactDOM from "react-dom/client" import App from "./App" import { api } from "./lib/pyrpc" ReactDOM.createRoot(document.getElementById("root")!).render( ) ``` ## 3. Call procedures [#3-call-procedures] ```tsx title="src/App.tsx" import { useState } from "react" import { api } from "./lib/pyrpc" export function App() { const [name, setName] = useState("") // @rpc.query maps to useQuery const { data: greeting, isLoading } = api.greet.useQuery({ name: "World" }) // @rpc.mutation maps to useMutation const createItem = api.create_item.useMutation() return (
{isLoading ?

Loading

:
{JSON.stringify(greeting)}
} setName(e.target.value)} /> {createItem.isSuccess &&
{JSON.stringify(createItem.data)}
}
) } ``` `useQuery` and `useMutation` are the standard TanStack Query hooks. All their options (`enabled`, `staleTime`, `onSuccess`, etc.) work exactly as documented in the [TanStack Query docs](https://tanstack.com/query/latest). ## Invalidating after a mutation [#invalidating-after-a-mutation] ```tsx const utils = api.useUtils() const createItem = api.create_item.useMutation({ onSuccess: () => { utils.list_items.invalidate() }, }) ``` ## TypeScript [#typescript] All parameter types, return types, and error shapes are inferred from your Python function signatures. There is nothing extra to annotate. ```ts // Python: @rpc.query \n def greet(name: str) -> dict: ... api.greet.useQuery({ name: "Ada" }) // correct api.greet.useQuery({ nme: "Ada" }) // type error -- typo caught at compile time ``` ## Client config [#client-config] `createReactClient` accepts the same config as the vanilla client: | Option | Type | Default | Description | | ------- | ------------------ | --------- | --------------------------------------------------------------------------- | | `links` | `Link[]` | required | Link pipeline; exactly one terminating link (`httpLink` or `httpBatchLink`) | | `kinds` | `ProcedureKindMap` | generated | Override generated procedure kinds | ## Full working examples [#full-working-examples] | Server | Source | | ------- | ------------------------------------------------------------------------------------------- | | FastAPI | [`examples/fastapi-react`](https://github.com/pyrpc/pyrpc/tree/main/examples/fastapi-react) | | Flask | [`examples/flask-react`](https://github.com/pyrpc/pyrpc/tree/main/examples/flask-react) | | Django | [`examples/django-react`](https://github.com/pyrpc/pyrpc/tree/main/examples/django-react) | # Svelte `@pyrpc/svelte` gives you typed `createQuery` / `createMutation` stores for every pyRPC procedure. Works with SvelteKit and plain Svelte. ## Installation [#installation] ```bash npm install @pyrpc/svelte @tanstack/svelte-query ``` ## Project structure [#project-structure] ``` my-app/ src/ lib/ pyrpc.ts ← createSvelteClient, import everywhere routes/ +layout.svelte ← QueryClientProvider wrapper +page.svelte ← api.greet.createQuery() etc. ``` ## 1. Create the client [#1-create-the-client] ```ts title="src/lib/pyrpc.ts" import { createSvelteClient, httpBatchLink } from "@pyrpc/svelte" import type { Types } from "@pyrpc/types" export const api = createSvelteClient({ links: [ httpBatchLink({ url: import.meta.env.VITE_API_URL ?? "http://localhost:8000", }), ], }) ``` ## 2. Wrap the layout [#2-wrap-the-layout] Svelte uses TanStack Svelte Query's `QueryClientProvider` directly in the layout, the standard approach: ```svelte title="src/routes/+layout.svelte" ``` ## 3. Call procedures in pages [#3-call-procedures-in-pages] ```svelte title="src/routes/+page.svelte" {#if $greeting.isPending}

Loading…

{:else}
{JSON.stringify($greeting.data)}
{/if}
{JSON.stringify($item.data)}
{#if $createItem.isSuccess}
{JSON.stringify($createItem.data)}
{/if} ``` Prefix the store with `$` to subscribe to its value, this is standard Svelte store syntax. ### Reactive args [#reactive-args] When query args come from reactive state, use a getter function: ```ts let itemId = 1 const item = api.get_item.createQuery(() => ({ id: itemId })) // changing itemId triggers a refetch automatically ``` ## TypeScript [#typescript] All parameter and return types are inferred from Python: ```ts api.greet.createQuery() // ✓ api.read_item.createQuery(() => ({ item_id: 42, q: "test" })) // ✓ api.read_item.createQuery(() => ({ item_id: "oops" // ✗, type error })) ``` ## Client config [#client-config] | Option | Type | Default | Description | | ------- | ------------------ | --------- | --------------------------------------------------------------------------- | | `links` | `Link[]` | required | Link pipeline; exactly one terminating link (`httpLink` or `httpBatchLink`) | | `kinds` | `ProcedureKindMap` | generated | Override generated procedure kinds | ## Invalidating after a mutation [#invalidating-after-a-mutation] Queries are cached under the key `[QUERY_KEY_PREFIX, procedure, input]` (`QUERY_KEY_PREFIX` is exported from `@pyrpc/svelte`). To invalidate after a mutation, use the shared TanStack Query client: ```svelte title="src/routes/+page.svelte" ``` ## Vanilla escape hatch [#vanilla-escape-hatch] Every procedure also stays callable as a plain promise via `api.client`, which is the underlying vanilla client: ```ts const greeting = await api.client.greet({ name: "World" }) ``` ## Full working examples [#full-working-examples] | Server | Source | | ------- | --------------------------------------------------------------------------------------------- | | FastAPI | [`examples/fastapi-svelte`](https://github.com/pyrpc/pyrpc/tree/main/examples/fastapi-svelte) | | Flask | [`examples/flask-svelte`](https://github.com/pyrpc/pyrpc/tree/main/examples/flask-svelte) | | Django | [`examples/django-svelte`](https://github.com/pyrpc/pyrpc/tree/main/examples/django-svelte) | # Vue `@pyrpc/vue` gives you typed `createQuery` / `createMutation` composables for every pyRPC procedure. No hand-written types, no schema files. ## Installation [#installation] ```bash npm install @pyrpc/vue @tanstack/vue-query ``` ## Project structure [#project-structure] ``` my-app/ src/ pyrpc.ts ← createPyrpcVue, import this everywhere main.ts ← createApp().use(pyrpc.plugin).mount() App.vue ← pyrpc.greet.createQuery() etc. ``` ## 1. Create the client [#1-create-the-client] ```ts title="src/pyrpc.ts" import { createPyrpcVue, httpBatchLink } from "@pyrpc/vue" import type { Types } from "@pyrpc/types" export const pyrpc = createPyrpcVue({ links: [ httpBatchLink({ url: import.meta.env.VITE_API_URL ?? "http://localhost:8000", }), ], }) ``` ## 2. Install the plugin [#2-install-the-plugin] Vue uses a plugin instead of a Provider component. Install it once on the root app, this sets up the TanStack Vue Query cache: ```ts title="src/main.ts" import { createApp } from "vue" import App from "./App.vue" import { pyrpc } from "./pyrpc" createApp(App).use(pyrpc.plugin).mount("#app") ``` ## 3. Call procedures in components [#3-call-procedures-in-components] ```vue title="src/App.vue" ``` ### Reactive args [#reactive-args] When a query's args depend on reactive state, pass them as a getter function so TanStack Vue Query re-fetches when they change: ```ts const userId = ref(1) const { data: user } = pyrpc.get_user.createQuery(() => ({ id: userId.value })) // changes to userId.value automatically trigger a refetch ``` ## TypeScript [#typescript] All parameter and return types are inferred from your Python function signatures: ```ts pyrpc.greet.createQuery() // ✓ name has a default pyrpc.read_item.createQuery(() => ({ // ✓ item_id: 42, q: "test", })) pyrpc.read_item.createQuery(() => ({ item_id: "not-a-number", // ✗, type error })) ``` ## Client config [#client-config] `createPyrpcVue` accepts the same options as the vanilla client: | Option | Type | Default | Description | | ------- | ------------------ | --------- | --------------------------------------------------------------------------- | | `links` | `Link[]` | required | Link pipeline; exactly one terminating link (`httpLink` or `httpBatchLink`) | | `kinds` | `ProcedureKindMap` | generated | Override generated procedure kinds | ## Full working examples [#full-working-examples] | Server | Source | | ------- | --------------------------------------------------------------------------------------- | | FastAPI | [`examples/fastapi-vue`](https://github.com/pyrpc/pyrpc/tree/main/examples/fastapi-vue) | | Flask | [`examples/flask-vue`](https://github.com/pyrpc/pyrpc/tree/main/examples/flask-vue) | | Django | [`examples/django-vue`](https://github.com/pyrpc/pyrpc/tree/main/examples/django-vue) | # Community Adapters Community adapters integrate pyRPC with additional frameworks, platforms, or hosting environments. > Coming soon: a curated list of community adapters. In the meantime, feel free to open a discussion or PR to add yours. # Django The Django adapter wires pyRPC into your URLconf with `mount_django(urlpatterns)`. It uses Django's native `async def` views (Django 4.2+), so there is no bridge or wrapper needed. ## Requirements [#requirements] * Python 3.11+ * Django 4.2+ * `pyrpc-core[django]` ## 1. Install [#1-install] ```bash uv add pyrpc-core[django] # or pip install "pyrpc-core[django]" ``` For CORS during local development: ```bash uv add django-cors-headers # or pip install django-cors-headers ``` ## 2. Project scaffold [#2-project-scaffold] If you are starting a new project, follow the [official Django tutorial](https://docs.djangoproject.com/en/stable/intro/tutorial01/) to create the project structure: ```bash django-admin startproject myproject cd myproject python manage.py startapp myapp ``` The minimum file structure pyRPC needs: ``` myproject/ manage.py myproject/ __init__.py settings.py urls.py wsgi.py ``` ## 3. Settings [#3-settings] Add `corsheaders` and your app to `INSTALLED_APPS`. Put `CorsMiddleware` first in `MIDDLEWARE` -- this is required by [django-cors-headers](https://github.com/adamchainz/django-cors-headers). ```python title="myproject/settings.py" INSTALLED_APPS = [ "corsheaders", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "myproject", ] MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", # must be first "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] CORS_ALLOWED_ORIGINS = [ "http://localhost:3000", # React / Next.js "http://localhost:5173", # Vue / Svelte (Vite) ] CORS_ALLOW_CREDENTIALS = True ``` ## 4. Write procedures [#4-write-procedures] Procedures are functions decorated with `@rpc.query` or `@rpc.mutation`. Django 4.2+ runs `async def` procedures as native async views with no bridge needed; plain `def` procedures work too. ```python title="myproject/views.py" from django.http import HttpResponse from pyrpc_core import rpc def index(request): return HttpResponse("

Django + pyRPC

") @rpc.query async def greet(name: str = "World") -> dict: return {"message": f"Hello, {name}!", "framework": "Django"} @rpc.query async def read_item(item_id: int, q: str = None) -> dict: return {"item_id": item_id, "q": q} @rpc.mutation async def create_item(name: str, description: str = None) -> dict: return {"name": name, "description": description, "created": True} ``` ## 5. Wire the URLs [#5-wire-the-urls] Call `mount_django(urlpatterns)` in your URLconf. The import of `views` at the top is required -- it executes the `@rpc` decorators. Without it the procedures are never registered and `/rpc` returns an empty schema. ```python title="myproject/urls.py" from django.contrib import admin from django.urls import path from pyrpc_django import mount_django from . import views # required: triggers @rpc decorator execution urlpatterns = [ path("admin/", admin.site.urls), path("", views.index, name="index"), ] mount_django(urlpatterns) ``` `mount_django` appends two URL patterns to the list: * `POST /rpc` -- procedure dispatch (native async view) * `GET /rpc` -- schema introspection (used by `pyrpc dev` and the Python client) ## 6. Start the dev server [#6-start-the-dev-server] Run `pyrpc dev` from the directory containing `manage.py` to generate and keep TypeScript types in sync: ```bash pyrpc dev --yes --framework django --module myproject.views --client ../client ``` First run: the wizard asks for your backend framework (Django), the path to `manage.py`, and a **types module** - the module whose import registers your `@rpc` procedures (usually the `views.py` that declares them, e.g. `myproject.views`; Django's settings and manage.py register nothing). This is written to `pyrpc.json`. Every run after reads it automatically. `pyrpc dev` launches Django's own development server (`manage.py runserver`) for you, generates `__pyrpc.ts` in the client project, and regenerates it on every `.py` save. If you prefer to manage the server yourself, run it separately: ```bash python manage.py runserver ``` Django runs the async RPC views natively -- no separate WSGI/ASGI bridge required. ## 7. Connect the frontend [#7-connect-the-frontend] ```ts title="lib/pyrpc.ts" import { createReactClient, httpBatchLink } from "@pyrpc/react" import type { Types } from "@pyrpc/types" export const api = createReactClient({ links: [ httpBatchLink({ url: "http://localhost:8000", }), ], }) ``` See [React](/docs/client/adapters/react), [Next.js](/docs/client/adapters/nextjs), [Vue](/docs/client/adapters/vue), or [Svelte](/docs/client/adapters/svelte) for full frontend setup per framework. ## How it works [#how-it-works] * `@rpc.query` / `@rpc.mutation` register the function in the global procedure registry and tag it with a kind. * `mount_django(urlpatterns)` appends the two RPC routes to your URLconf. The views are `csrf_exempt` and return JSON directly. * The `from . import views` line in `urls.py` is what causes Python to execute the module and run the `@rpc` decorators. This is the same mechanism Django uses for signals and `AppConfig.ready()`. * `async def` procedures use Django 4.2+'s native async view dispatch. There is no `anyio.run` wrapper. ## Custom router [#custom-router] If you want to isolate procedures by app rather than using the global default router: ```python title="myapp/procedures.py" from pyrpc_core import Router router = Router() @router.query async def list_items() -> list: return [] @router.mutation async def create_item(name: str) -> dict: return {"name": name, "created": True} ``` ```python title="myproject/urls.py" from myapp.procedures import router from pyrpc_django import mount_django urlpatterns = [] mount_django(urlpatterns, router=router) ``` ## Full working examples [#full-working-examples] | Example | Frontend | Source | | ---------------- | ------------------------------ | ------------------------------------------------------------------------------------------- | | Django + React | React + TanStack Query | [`examples/django-react`](https://github.com/pyrpc/pyrpc/tree/main/examples/django-react) | | Django + Next.js | Next.js App Router + RSC | [`examples/django-nextjs`](https://github.com/pyrpc/pyrpc/tree/main/examples/django-nextjs) | | Django + Vue | Vue 3 + TanStack Vue Query | [`examples/django-vue`](https://github.com/pyrpc/pyrpc/tree/main/examples/django-vue) | | Django + Svelte | Svelte + TanStack Svelte Query | [`examples/django-svelte`](https://github.com/pyrpc/pyrpc/tree/main/examples/django-svelte) | # FastAPI The FastAPI adapter mounts pyRPC onto a FastAPI app. Every registered procedure becomes callable at `POST /rpc`. Queries map to `useQuery` and mutations to `useMutation` on the frontend, no extra config needed. ## 1. Install [#1-install] ```bash uv add pyrpc-core[fastapi] # or pip install "pyrpc-core[fastapi]" ``` ## 2. Write the server [#2-write-the-server] ```python title="main.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"], # your frontend origin 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) ``` ## 3. Start the dev server [#3-start-the-dev-server] From the directory containing `main.py`: ```bash pyrpc dev ``` First run launches a short wizard - backend framework (FastAPI is preselected when detected) and entry point, then client root and frontend framework - and writes `pyrpc.json`. Every subsequent run reads that file, no prompts. To skip the wizard entirely: ```bash # auto-detect everything pyrpc dev --yes # fully explicit: good for CI pyrpc dev --yes --framework fastapi --module main --client ../client ``` `pyrpc dev` starts uvicorn with your `module[:app]` entry point, generates `__pyrpc.ts` at the client project root, and re-generates it on every `.py` save. If a server is already running on that port, it skips starting uvicorn and only runs the type watcher. ## 4. Connect the frontend [#4-connect-the-frontend] Install the adapter for your frontend framework: ```bash # React npm install @pyrpc/react @tanstack/react-query # Next.js npm install @pyrpc/next @tanstack/react-query # Vue npm install @pyrpc/vue @tanstack/vue-query # Svelte npm install @pyrpc/svelte @tanstack/svelte-query ``` Then create the typed client, the import from `@pyrpc/types` resolves to the generated `__pyrpc.ts` in your client project: ```ts title="lib/pyrpc.ts" import { createReactClient, httpBatchLink } from "@pyrpc/react" import type { Types } from "@pyrpc/types" export const api = createReactClient({ links: [ httpBatchLink({ url: "http://localhost:8000", }), ], }) ``` See the [React](/docs/client/adapters/react), [Next.js](/docs/client/adapters/nextjs), [Vue](/docs/client/adapters/vue), or [Svelte](/docs/client/adapters/svelte) docs for complete setup per framework. ## How it works [#how-it-works] * `@rpc.query` / `@rpc.mutation` register the function in a global procedure registry and tag it with a kind. * `mount_fastapi(app)` adds two routes: * `POST /rpc`: procedure dispatch * `GET /rpc`: schema introspection (used by `pyrpc dev` to generate types) * The kind tag (`query` vs `mutation`) flows through codegen into the generated `__pyrpc.ts` file, which is how the frontend adapter knows which TanStack hook to expose on each procedure. ## CORS origins by frontend [#cors-origins-by-frontend] | Frontend | Default dev origin | | ------------- | ----------------------- | | React (CRA) | `http://localhost:3000` | | React (Vite) | `http://localhost:5173` | | Next.js | `http://localhost:3000` | | Vue (Vite) | `http://localhost:5173` | | Svelte (Vite) | `http://localhost:5173` | ## Routers [#routers] For larger projects, organize procedures into separate routers: ```python from pyrpc_core import Router from pyrpc_fastapi import mount_fastapi users = Router() items = Router() @users.query def get_user(user_id: int): ... @items.mutation def create_item(name: str): ... # Merge into the default router before mounting from pyrpc_core import default_router default_router.include(users, prefix="users") default_router.include(items, prefix="items") mount_fastapi(app) ``` ## Full working examples [#full-working-examples] | Example | Frontend | Source | | ----------------- | ------------------------------ | --------------------------------------------------------------------------------------------- | | FastAPI + React | React + TanStack Query | [`examples/fastapi-react`](https://github.com/pyrpc/pyrpc/tree/main/examples/fastapi-react) | | FastAPI + Next.js | Next.js App Router + RSC | [`examples/fastapi-nextjs`](https://github.com/pyrpc/pyrpc/tree/main/examples/fastapi-nextjs) | | FastAPI + Vue | Vue 3 + TanStack Vue Query | [`examples/fastapi-vue`](https://github.com/pyrpc/pyrpc/tree/main/examples/fastapi-vue) | | FastAPI + Svelte | Svelte + TanStack Svelte Query | [`examples/fastapi-svelte`](https://github.com/pyrpc/pyrpc/tree/main/examples/fastapi-svelte) | # Flask The Flask adapter mounts pyRPC onto a Flask app. Flask is sync-only, so the adapter handles async procedure execution internally, your procedure code stays the same. ## 1. Install [#1-install] ```bash uv add pyrpc-core[flask] # or pip install "pyrpc-core[flask]" ``` This also installs `flask-cors`, which you'll need for local frontend development. ## 2. Write the server [#2-write-the-server] ```python title="main.py" from flask import Flask from flask_cors import CORS from pyrpc_core import rpc from pyrpc_flask import mount_flask app = Flask(__name__) CORS(app, origins=["http://localhost:3000"]) # your frontend origin @rpc.query def greet(name: str = "World") -> dict: return {"message": f"Hello, {name}!", "framework": "Flask"} @rpc.query def read_item(item_id: int, q: str = None) -> dict: return {"item_id": item_id, "q": q} @rpc.mutation def create_item(name: str, description: str = None) -> dict: return {"name": name, "description": description, "created": True} mount_flask(app) if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=5000) ``` ## 3. Start the dev server [#3-start-the-dev-server] ```bash pyrpc dev ``` First run: a short wizard asks for your backend framework (Flask is preselected when detected) and entry point, then client root and frontend framework, and writes `pyrpc.json`. Every run after: reads `pyrpc.json` automatically. To skip the wizard: ```bash # auto-detect pyrpc dev --yes # fully explicit pyrpc dev --yes --framework flask --module main --client ../client ``` `pyrpc dev` serves the app with Flask's own dev server (`flask run` on your `module:app` - no WSGI-to-ASGI bridge involved; default `http://127.0.0.1:8000/rpc`), generates `__pyrpc.ts` at the client project root, and re-generates it on every `.py` save. ## 4. Connect the frontend [#4-connect-the-frontend] ```ts title="lib/pyrpc.ts" import { createReactClient, httpBatchLink } from "@pyrpc/react" import type { Types } from "@pyrpc/types" export const api = createReactClient({ links: [ httpBatchLink({ url: "http://localhost:8000", }), ], }) ``` Note: the URL must match the port your Flask app is served on. `pyrpc dev` uses port `8000` by default; if you run the app directly with `app.run(port=5000)` instead, use `5000`. See [React](/docs/client/adapters/react), [Next.js](/docs/client/adapters/nextjs), [Vue](/docs/client/adapters/vue), or [Svelte](/docs/client/adapters/svelte) for full frontend setup. ## How it works [#how-it-works] * `@rpc.query` / `@rpc.mutation` register the function in the global registry with its kind. * `mount_flask(app)` adds two routes to the Flask app: * `POST /rpc`: procedure dispatch (async execution via `anyio.run`) * `GET /rpc`: schema introspection * Flask is sync-only, `mount_flask` runs async procedure execution with `anyio.run` internally. You don't need to change anything. ## CORS origins by frontend [#cors-origins-by-frontend] | Frontend | Default dev origin | | ------------- | ----------------------- | | React (CRA) | `http://localhost:3000` | | React (Vite) | `http://localhost:5173` | | Next.js | `http://localhost:3000` | | Vue (Vite) | `http://localhost:5173` | | Svelte (Vite) | `http://localhost:5173` | ## Full working examples [#full-working-examples] | Example | Frontend | Source | | --------------- | ------------------------------ | ----------------------------------------------------------------------------------------- | | Flask + React | React + TanStack Query | [`examples/flask-react`](https://github.com/pyrpc/pyrpc/tree/main/examples/flask-react) | | Flask + Next.js | Next.js App Router + RSC | [`examples/flask-nextjs`](https://github.com/pyrpc/pyrpc/tree/main/examples/flask-nextjs) | | Flask + Vue | Vue 3 + TanStack Vue Query | [`examples/flask-vue`](https://github.com/pyrpc/pyrpc/tree/main/examples/flask-vue) | | Flask + Svelte | Svelte + TanStack Svelte Query | [`examples/flask-svelte`](https://github.com/pyrpc/pyrpc/tree/main/examples/flask-svelte) | # Overview # Adapters [#adapters] Adapters integrate pyRPC with your server runtime. They: * expose a single HTTP endpoint (by default `POST /rpc`) * forward JSON-RPC payloads to the pyRPC interpreter * return JSON-RPC responses from your procedures ## Available Adapters [#available-adapters] * **[FastAPI](/docs/server/adapters/fastapi)** - Mount pyRPC on a FastAPI app * **[Flask](/docs/server/adapters/flask)** - Mount pyRPC on a Flask app * **[Django](/docs/server/adapters/django)** - Mount pyRPC on a Django app * **[Standalone](/docs/server/adapters/standalone)** - Use the minimal ASGI app directly Each adapter uses the same core registry of procedures registered with `@rpc`. # Standalone For minimal deployments or ASGI-first frameworks, use the built-in `PyRPCAsgiApp`. ## Basic Usage [#basic-usage] ```python from pyrpc_core import PyRPCAsgiApp app = PyRPCAsgiApp() ``` Or use the pre-created instance: ```python from pyrpc_core import asgi_app ``` This ASGI app: * handles only `POST /rpc` * reads the JSON body * calls `handle_request(payload)` * returns a JSON-RPC response ## Mounting Under a Prefix [#mounting-under-a-prefix] Use your ASGI framework/router to mount the app at a prefix (e.g. `/api/rpc`). The app itself listens on `/rpc` internally; the outer router controls the external path.