Eight diagrams. Seven packages. One framework. Let's walk through pyrpc's architecture from top to bottom, using the LikeC4 diagrams we created.
To follow along, run npx likec4 start architecture and open the views as we go.
Level 1: System Landscape
The broadest view. Three actors interact with pyrpc:
- Python Developer — writes
@rpc-decorated functions and runs the CLI - TypeScript Developer — consumes the typed client in frontend apps
- JSON-RPC 2.0 Protocol — the wire format pyrpc conforms to
This view answers: what are the boundaries of our system? The Python developer is inside pyrpc's world (they write the endpoints). The TypeScript developer is outside (they only see generated types and the client library). JSON-RPC 2.0 is a constraint — pyrpc must speak this language correctly.
Level 2: Container Diagram
Zoom into pyrpc. Seven containers (packages) form the system:
pyrpc pyrpc-core — Router, Interpreter, CLI, ASGI, Python client (10 components) pyrpc-fastapi — mount_fastapi() adapter (26 lines) pyrpc-flask — mount_flask() adapter with anyio.run() sync bridge (33 lines) pyrpc-django — mount_django() adapter with native async views (38 lines) pyrpc-codegen — Python→TypeScript code generation pipeline (5 components) @pyrpc/client — TypeScript Proxy client, CLI sync, postinstall wizard @pyrpc/types — Generated TS type definitions (src/index.ts)
The key insight: pyrpc-core is the center of the universe. Every adapter, the codegen, and the CLI all talk to core. No container talks to another container directly without going through core. Two config files support the system: pyrpc.json (Python-side config) and pyrpc-client.json (TypeScript-side config).
Level 3a: pyrpc-core Internals
The most detailed view. pyrpc-core contains 10 components:
| Component | File | Job |
|---|---|---|
| Router | registry.py | Thread-safe registry of named Procedures. Supports merge() and reload_module() for hot-reload. |
| Procedure | procedure.py | Compiled handler wrapping a user function. Pre-builds Pydantic TypeAdapters at init time. |
| @rpc decorator | decorators.py | Alias for default_router.rpc(). Compiles function into Procedure at import time. |
| @model decorator | decorators.py | Alias for pydantic.dataclasses.dataclass. Marks classes for JSON Schema generation. |
| handle_request() | interpreter.py | Core dispatcher: validate envelope → lookup Procedure → execute → return response. |
| get_registry_schema() | introspection.py | Single source of truth for both runtime introspection and TypeScript codegen. |
| RpcRequest / RpcResponse | models.py | Pydantic v2 models for the JSON-RPC 2.0 envelope. |
| PyRPCAsgiApp | transport/asgi.py | Standalone ASGI app for POST/GET /rpc with CORS headers. |
| CLI | cli.py | Typer CLI: dev, serve, inspect, pull, codegen. 764 lines. |
| Python RPCClient | client/python_client.py | Dynamic Python client using httpx. Auto-fetches schema for method introspection. |
Level 3b: pyrpc-codegen Pipeline
The codegen package has a 5-component pipeline: generate_typescript_client() collects schemas and orchestrates → _pytype_to_ts() converts Python types to TypeScript → client.ts.j2 renders method signatures via Jinja2 → jsonschema_ts converts Pydantic JSON schemas to TS interfaces → save_typescript_client() writes to @pyrpc/types/src/index.ts.
The @pyrpc/client internals are notable for the Proxy pattern — createClient<Types>() creates a Proxy that intercepts any property access. Calling client.myMethod({ arg }) Just Works without any pre-declared method list, because the Proxy dynamically translates the call at runtime into a JSON-RPC 2.0 POST request.
Level 4: Adapter Pattern Comparison
This view puts all four adapters side by side:
| Adapter | Lines | Async Strategy |
|---|---|---|
| PyRPCAsgiApp | ~80 | Native async ASGI |
| mount_fastapi() | 26 | Native async (FastAPI) |
| mount_flask() | 33 | anyio.run() sync-to-async bridge |
| mount_django() | 38 | Django async views + @csrf_exempt |
Each adapter does exactly two things: POST /rpc calls handle_request(), GET /rpc calls get_registry_schema(). Zero protocol logic. Zero business logic. Just HTTP plumbing. This "thin shell" pattern is why adding a new adapter takes ~30 lines of code.
Dynamic Views: Tracing Workflows
Three dynamic views capture the most important sequences:
RPC Call Flow (7 steps): Proxy → HTTP POST → Adapter → envelope validation → Router lookup → Procedure execution with TypeAdapter validation → response back. Error path: PyRPCError thrown on client.
Codegen Flow (10 steps): @rpc registration → CLI triggers codegen → get_registry_schema() → type conversion → Jinja2 rendering → jsonschema_ts model generation → file write.
Dev Loop Flow (12 steps): pyrpc dev → read config → import module → start watcher + server → file change detected → debounce 300ms → reload_module() → schema extraction → type regeneration → console update.
The next post in this series traces a single RPC call end-to-end — from the TypeScript Proxy through to the Python function and back.

pyRPC