pyrpc is designed to work across three fundamentally different project structures. Each structure has different constraints, different workflows, and different expectations about how types flow from the Python server to the TypeScript client. Getting all three right meant designing the config system, type generation, and migration logic to handle each case without special-casing any of them.
Architecture 1: Monorepo
This is the most common pyrpc setup. The Python server and TypeScript client live in the same repository, usually in adjacent directories:
my-app/
server/
main.py
pyrpc.json # client_root: "../client"
client/
package.json # depends on @pyrpc/client, @pyrpc/types
src/
client.ts
node_modules/
@pyrpc/types/src/index.ts ← generated hereIn this architecture, pyrpc dev runs from the server directory (or the root, if pyrpc.json is there). The config file pointsclient_root at the client directory. Types are written directly into node_modules/@pyrpc/types/src/index.ts inside the client project.
The key advantage: everything runs from one command.pyrpc dev starts the server, watches files, regenerates types on changes, and provides an interactive console — all in a single terminal session. The frontend developer runs their dev server separately (Vite, Next.js, etc.) and imports the types that are kept up to date automatically.
This is the “tRPC experience” for Python backends — the workflow that inspired pyrpc. It’s the fastest feedback loop: change a Python function, save the file, and the TypeScript types update within 300ms (the watcher debounce).
Architecture 2: Separate repositories
Many teams work with separate repos for the backend and frontend. This is common in larger organizations, or when the backend is consumed by multiple frontend projects:
backend/
main.py
pyrpc.json # client_root: not set - no frontend in this repo
frontend/
package.json # depends on @pyrpc/client, @pyrpc/types
src/
client.ts
node_modules/
@pyrpc/types/src/index.ts ← generated via pyrpc codegenIn this architecture, the frontend cannot directly access the backend’s file system — at least not in CI. The workflow becomes:
- Backend developer runs
pyrpc pullto extract the schema as JSON - The JSON schema file is checked into a shared location (another repo, an artifact registry, or a Git submodule)
- Frontend CI runs
pyrpc codegen schema.jsonto generate types
The pull and codegen commands are designed for exactly this decoupled workflow. pyrpc pull serializes the schema without starting a server; pyrpc codegen can read from a file, a URL, or a Python module. The two-step split means the backend CI can commit a schema file, and the frontend CI can generate types without needing Python installed.
What’s not built yet: The client-side npx pyrpc typescommands (init, pull, watch) that would make this workflow as smooth as the monorepo case. Currently, the frontend setup requires manual pyrpc codegen calls or a CI script. A future release will add npx pyrpc types init to configure the server URL andnpx pyrpc types watch to poll for schema changes.
Architecture 3: Published npm package (@pyrpc/types as a published artifact)
In the most mature setup, the generated TypeScript types are published as an npm package. Multiple frontend projects consume the same types without needing access to the backend or its CI artifacts:
backend/
main.py
pyrpc.json
# CI/CD pipeline:
# 1. Run pyrpc pull → schema.json
# 2. Run pyrpc codegen schema.json --output /tmp/types/index.ts
# 3. Publish /tmp/types as @my-org/my-api-types@1.2.3
frontend-a/
package.json # depends on @my-org/my-api-types
src/
client.ts # import type { Types } from "@my-org/my-api-types"
frontend-b/
package.json # depends on @my-org/my-api-types
src/
client.ts # same types, same versionThis architecture is for teams that want strict versioning of the API contract. The types are published with semver, and frontend projects opt into updates by bumping their dependency version. It’s the slowest feedback loop (changes flow through CI/CD on every push) but the most controlled.
pyrpc’s save_typescript_client() function is the right API for this workflow — it takes a schema dict and an output path, generates the file, and returns. CI scripts call it programmatically, not through the CLI. The fact that it requires an absolute path is a feature here: CI environments have well-known working directories, and the absolute path requirement prevents “where did my file go?” confusion in headless environments.
How the config system supports all three
The pyrpc.json config system was designed with all three architectures in mind:
client_rootis required — even in architectures 2 and 3, the config file stores the local client path during development. It’s only in CI thatclient_rootmight be overridden or ignored.- Path resolution is config-relative — this matters most in monorepos (architecture 1) where pyrpc might be run from a subdirectory. In separate-repo and published-npm setups, the config file is usually at the repo root, so CWD-relative and config-relative produce the same result.
- No pyproject.toml dependency —
pyrpc.jsonis a standalone file. In architecture 2, the frontend repo doesn’t even have apyproject.toml(it’s JavaScript-only). A TOML-based config would fail to parse there. A JSON-based config file is language-agnostic.
Why we built server-side codegen first
Architecture 1 (monorepo) covers the majority of pyrpc users. It’s also the hardest to get right — it needs file watching, debounced regeneration, path resolution, and migration handling. By solving the hardest case first, we built a foundation that works for all three architectures. The futurenpx pyrpc types client-side CLI is additive — it will call the same pyrpc codegen endpoint, just from the frontend instead of the backend.
Each architecture serves a different stage of team maturity. Start with monorepo (fastest iteration). Split into separate repos when team boundaries demand it. Publish an npm package when you need strict versioning for multiple consumers. pyrpc supports all three without branching config paths or conditional logic.

pyRPC