pyRPC
← Back to Blog

Three deployment architectures for pyrpc

·9 min read

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 here

In 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 codegen

In this architecture, the frontend cannot directly access the backend’s file system — at least not in CI. The workflow becomes:

  1. Backend developer runs pyrpc pull to extract the schema as JSON
  2. The JSON schema file is checked into a shared location (another repo, an artifact registry, or a Git submodule)
  3. Frontend CI runs pyrpc codegen schema.json to 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 version

This 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_root is required — even in architectures 2 and 3, the config file stores the local client path during development. It’s only in CI that client_root might 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 dependencypyrpc.jsonis a standalone file. In architecture 2, the frontend repo doesn’t even have a pyproject.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.