pyRPC
← Back to Blog

A Visual Tour of pyrpc's Architecture

·10 min read

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:

ComponentFileJob
Routerregistry.pyThread-safe registry of named Procedures. Supports merge() and reload_module() for hot-reload.
Procedureprocedure.pyCompiled handler wrapping a user function. Pre-builds Pydantic TypeAdapters at init time.
@rpc decoratordecorators.pyAlias for default_router.rpc(). Compiles function into Procedure at import time.
@model decoratordecorators.pyAlias for pydantic.dataclasses.dataclass. Marks classes for JSON Schema generation.
handle_request()interpreter.pyCore dispatcher: validate envelope → lookup Procedure → execute → return response.
get_registry_schema()introspection.pySingle source of truth for both runtime introspection and TypeScript codegen.
RpcRequest / RpcResponsemodels.pyPydantic v2 models for the JSON-RPC 2.0 envelope.
PyRPCAsgiApptransport/asgi.pyStandalone ASGI app for POST/GET /rpc with CORS headers.
CLIcli.pyTyper CLI: dev, serve, inspect, pull, codegen. 764 lines.
Python RPCClientclient/python_client.pyDynamic 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:

AdapterLinesAsync Strategy
PyRPCAsgiApp~80Native async ASGI
mount_fastapi()26Native async (FastAPI)
mount_flask()33anyio.run() sync-to-async bridge
mount_django()38Django 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.