Before v0.13.0, createClient took transport options directly, URL, headers, batching behavior, all flattened into one options object. Every future feature (retries, auth tokens, request logging) would have meant another option, another conditional, another coupling between concerns that do not belong together. PR #136 replaced that with the design tRPC and urql normalized years ago: a link pipeline.
One terminating link, zero or more composable ones
A link is small on purpose. It sees an operation and either passes it along or ends the chain by producing a result:
export function httpLink(options: HttpLinkOptions): TerminatingLink {
const url = normalizeUrl(options.url);
return {
async request(operation: Operation): Promise<OperationResult> {
const response = await fetch(url, {
method: 'POST',
headers: CONTENT_TYPE,
body: JSON.stringify(operation),
});
return readJson<OperationResult>(response);
},
};
}The client enforces two rules: you must supply links, and exactly one may terminate. Everything else is composition territory, auth injection, retry with backoff, logging, request splitting. None of those belong in pyRPC core; now none of them have to be.
URL normalization lives at the terminator
One detail that quietly removed a class of support issues: normalizeUrl accepts "http://localhost:8000", "http://localhost:8000/", or even a URL already ending in /rpc, and always produces the correct endpoint:
function normalizeUrl(url: string): string {
const clean = url.replace(/\/+$/, '');
return clean.replace(/\/rpc$/i, '') + '/rpc';
}Previously a trailing slash could 404 against some deployments. The rule moved from “read the docs carefully” to “cannot be wrong.”
Adapters re-export, so imports stay canonical
All four framework adapters re-export the terminating links:
// @pyrpc/react, @pyrpc/next, @pyrpc/vue, @pyrpc/svelte
import { createReactClient, httpBatchLink } from "@pyrpc/react"
const api = createReactClient<Types>({
links: [httpBatchLink({ url: process.env.API_URL })],
})Users import everything from their adapter package; @pyrpc/client remains an implementation dependency rather than something every tutorial has to explain. The codegen template emits exactly this shape, so scaffolded projects start current.
What we deliberately did not build
No Link base class, no observable machinery, no middleware context objects. The T in tRPC’s design is the insight, not the framework around it: a { request(operation) } object composes, serializes trivially, and can be understood in one screenful. When non-terminating links arrive as first-class exports, they will slot into the pipeline users already have, no migration required.

pyRPC