Bidirectional tRPC

October 14, 2025

Typescript ·Turborepo ·tRPC

Recently, I needed to create two Typescript apps that communicate with each other in both directions. It’s a very niche use case and figuring it out was quite a pain, but eventually I managed to get it working. This post is mainly for me to remember what I did, but it may be useful for others as well.


The problem

You might think “this sound easy, I can just use turborepo’s ‘workspace:*’ package version syntax to import the RPC server type”, but no, this doesn’t work. Turborepo checks for circular dependencies and in this case it finds one.


Here’s an example project structure:

apps/one
    ├─ src
    │   ├─ server.ts
    │   └─ client.ts
    ├─ package.json
    └─ tsconfig.json
apps/two
    ├─ src
    │   ├─ server.ts
    │   └─ client.ts
    ├─ package.json
    └─ tsconfig.json
package.json

in this case, app one imports the RPC server type from two, which imports the RPC server type from one. This is a circular dependency and the build will fail.


The solution

The solution lies in a very little-known Typescript feature: project references. This feature allows you to split your project in smaller pieces (kind of like a monorepo).


To use it you simply1 need to update your tsconfig.json files to add this:

{
	"references": [
		{
			// or "../two" if you are in the "one" app.
			"path": "../one"
		}
	]
}

I’d also recommend to add a path alias to make it easier to import things.

{
	"compilerOptions": {
		// ...
		"paths": {
			"~one/rpc": ["../one/src/server.ts"]
		}
	}
}

This way, your tRPC client can look like this:

// apps/two/src/client.ts
import { createTRPCClient } from '@trpc/client';
import type { AppRouter } from '~one/rpc';

export const rpc = createTRPCClient<ServerRouter>({
	links: [
		// your links
	]
});

That’s it! It should now work™


1 It was not simple. I spent like 10 hours trying to figure this out. fml.

Copyright © 2016-2025 boris.foo (formerly borisnl.nl), All rights reserved.