baseUrl is the one piece of runtime configuration every pyRPC client needs to know about, and it exists because of a split between two channels that run at different times. At compile time it is a string in an options type. At runtime it is the prefix every fetch is built from. This post walks both sides and the normalization in between.
Why the option exists at all
pyRPC is transport-first: the client is a thin JSON-RPC caller, and the only thing the app must tell it is where the server is. Everything else about the API shape comes from generated types, not from config. So the surface stays minimal, one URL, plus optional headers:
interface ClientOptions {
baseUrl?: string;
headers?: HeadersInit | (() => Promise<HeadersInit> | HeadersInit);
}Compile time: a string, and nothing more
In the type channel, baseUrl is just string | undefined. The compiler enforces that you pass something shaped like a string, and that is the entire job of the type system here, the URL itself is never baked into the generated types. That is deliberate: the same __pyrpc.ts can point at a local dev server or a production domain without regenerating.
In practice the value arrives from the environment, and each framework has its own idiom for build-time inlining:
// Next.js, NEXT_PUBLIC_ vars are inlined at build time
createNextClient<Types>({
baseUrl: process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000',
})
// Vite, import.meta.env
createReactClient<Types>({
baseUrl: import.meta.env.VITE_API_URL ?? 'http://localhost:8000',
})The fallback ?? 'http://localhost:8000' is the compile-time convention that keeps baseUrl optional for local development while letting CI and production override it.
Runtime: the normalization pipeline
At runtime the string must become a working request URL. The client is forgiving about what you give it (trailing slashes and a redundant /rpc are both handled) so the same option works whether it comes from an env var, a proxy prefix, or a hard-coded dev URL:
baseUrl given fetch URL built ---------------------------------------------- "http://localhost:8000" http://localhost:8000/rpc "http://localhost:8000/" http://localhost:8000/rpc "http://localhost:8000/rpc" http://localhost:8000/rpc "http://localhost:8000/RPC" http://localhost:8000/rpc (undefined, browser) window.location.origin + "/rpc" (undefined, no window) throws on first request
The two lines that implement it:
const clean = baseUrl.replace(/\/+$/, ''); // strip trailing slashes this.url = clean.replace(/\/rpc$/i, '') + '/rpc'; // de-dup, then append
Strip trailing slashes, drop a /rpc suffix case-insensitively so it is never doubled, append /rpc. The same four lines run for every adapter because every adapter ultimately calls createClient.
Two fallbacks, one error
Because the browser shares an origin with the API in the common single-deploy case, the client falls back to window.location.origin when baseUrl is omitted, a Next.js app deployed on the same domain as its API works with zero configuration.
That fallback is browser-only. On the server there is no window, so omitting baseUrl leaves no URL at all, and the client fails loudly on the first request with a message that tells you exactly what to pass, rather than at construction time. Next.js Server Components hit this path when calling createCaller(): the server needs an explicit, usually absolute, baseUrl because there is no origin to inherit. The browser fallback, the server error, and the normalization are one coherent rule: compile time only checks the type; runtime resolves the URL.
Adapters pass it straight through
createReactClient, createVueClient, createSvelteClient, and createNextClient all accept the same ClientOptions and forward them to the underlying client unchanged. One contract, four entry points, which is why the normalization logic lives in exactly one place and every framework test asserts the same URL behavior.
Further reading
- One API object, how the client options flow through the framework adapters
- The RPC call flow, what happens after the URL is resolved
- Next.js: environment variables

pyRPC