A client that depends on your library in dependencies is a contract: it will be installed, bundled, and linked, forever. A client that lists your library as a peerDependency is a promise that the ecosystem provides it. Getting this split right is the difference between a library that works out of the box and one that fights its users. This post is the contract pyRPC actually ships.
The contract, package by package
@pyrpc/react @pyrpc/next
----------------------------------------------------------------------------------
dependencies @pyrpc/client ^0.12.0 same
@pyrpc/types ^0.12.0 + @pyrpc/react ^0.12.0
peerDependencies @tanstack/react-query ^5.0 @tanstack/react-query ^5.0
react >=18.0.0 react >=18.0.0
next >=14.0.0
devDependencies same set, pinned for tests same set, pinned for testsThe pattern: pyRPC owns everything it ships; the user’s stack is a peer. @pyrpc/client and @pyrpc/types are our code, so they are hard dependencies, installing @pyrpc/react must bring them along. React, Next.js, and TanStack Query are the user’s frameworks, so they are peers with version floors that state how recent your app must be.
“Internal” does not mean “owned”
TanStack Query is the interesting case. The hooks layer is thin (it is basically useQuery, useMutation, and query-client plumbing around the transport. When a library depends so directly on a peer, there is a temptation to re-export it so users get “everything in one import.” We chose not to, and the reason is the same one that puts @tanstack/react-query in peerDependencies rather than dependencies: an app can legitimately use TanStack Query outside pyRPC) for REST endpoints, optimistic caches, its own prefetching (and it should not end up with two copies of React hooks state fighting over the same cache. One query client, one cache, one version. That is the unit the peer contract protects.
The same reasoning is why we import @tanstack/react-query types in the hooks, not a wrapped copy. If the user upgrades React Query, the hooks keep working because they talk to the public API the user is already using.
The dependency that crossed over
The line between owned and not-owned is not frozen. @pyrpc/types started as a devDependency of the client packages (it was needed at compile time to build them, and the compiler erased the import, so users never needed it at runtime. Then the generated-file contract changed. The emitted __pyrpc.ts began referencing @pyrpc/types directly, and more importantly the framework adapters started importing it as a runtime value. A type-only dependency became a runtime dependency) the story in types: from dev-dep to runtime-dep. That is the mechanism by which things move across the line: when a package stops being compile-time-erased and starts being imported at runtime in the shipped artifacts, it earns a place in dependencies.
The Python side: extras as the peer contract
The Python package expresses the same idea differently, because pip has no peerDependencies. Instead, the framework integrations live behind extras: pyrpc-core[fastapi] pulls in the FastAPI scaffolding; Django and Flask get the same treatment. The core is framework-agnostic (it ships JSON-RPC handling and schema work with no server framework) and each server framework is opt-in. Install what you use, and never install four servers to use one. The role is identical to a peer range: pyRPC stays small, and the framework the user chose is respected as the user’s call.
What this means for a pyRPC app
- Add
@pyrpc/reactand npm pulls@pyrpc/clientand@pyrpc/typesautomatically, you never install those by hand. - You install
@tanstack/react-queryyourself and wire oneQueryClientProvider, the one every app would have anyway. - npm warnings about unmatched peer ranges are real signals: you are below the supported floor for React or Next.js, and upgrading your framework is the fix.
Further reading
- Why TanStack Query?, the decision that created the peer dependency
- Types: from dev-dep to runtime-dep, how the line gets crossed
- npm: peerDependencies
- TanStack Query docs
- tRPC docs, the peer-contract precedent

pyRPC