Image for Using TRPC with Cloudflare Workers in a monorepo

Using TRPC with Cloudflare Workers in a monorepo

In this article we will learn about how we can integrate TRPC with cloudflare workers in a monorepo.

What is TRPC?

TRPC is a tool via which you can build typesafe APIs without having to generate any schema that you have to keep in sync with the server and client.

It essentially reduces the logical distance between the client and the server by allowing you to call your APIs as if they were local functions.

What is a monorepo?

If you are not familiar with a monorepo, it is a codebase that contains multiple projects, such as a frontend and a backend, in a single repository. This can be useful for sharing code between projects, managing dependencies, and simplifying the development process.


To create a monorepo, there are a lot of options, but for this experiment we will use Turborepo .

To get started, you can either clone this repo, or run the following command to bootstrap a new project:

Terminal window
pnpm dlx create-turbo@latest --example https://github.com/bimsina/vite-react-cf-starter

After you’ve cloned the project, the backend code should look something like this:

apps/server/src/index.ts
import { sharedString } from '@repo/utils';
import { corsifyResponse } from './cors';
export default {
async fetch(request, env, ctx): Promise<Response> {
return corsifyResponse(
new Response(
JSON.stringify({
message: 'Hello from Cloudflare Worker!',
shared: sharedString,
}),
{
headers: {
'Content-Type': 'application/json',
},
},
),
request,
env,
);
},
} satisfies ExportedHandler<Env>;

This is what would look like calling the backend from the frontend:

apps/client/src/App.tsx
fetch("http://localhost:8787/")
.then((res) => res.json())
.then((data) => {
// handle the response
const { message, shared } = data;
console.log(message, shared);
});

As you can see this works, but here are some of the problems with this approach:

The API is not typesafe.

  • What if the backend changes the response shape?

  • What if the frontend parses the response incorrectly?

The API is not easily documented.

  • What if the frontend developer doesn’t know what the expected input is?

Enter TRPC. With TRPC, you can call your backend APIs as if they were local functions. This means the API called is typesafe and the input and output schema is well documented.


Let’s first setup TRPC in the server

  1. Installing packages
apps/server
pnpm add @trpc/server@next zod

Notice we have installed zod as well, which will help us in validating the user input.

  1. Define the router
apps/server/src/trpc.ts
import { initTRPC } from "@trpc/server";
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
  1. Initialize the router instance
apps/server/src/router.ts
import { router } from "./trpc";
export const appRouter = router({
// ...
});
export type AppRouter = typeof appRouter;
  1. Add a query
apps/server/src/router.ts
import { publicProcedure, router } from './trpc';
const appRouter = router({
greetUser: publicProcedure.query(() => {
return {
message: 'Hello, user!',
};
}),
});
// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof appRouter;
  1. Validate the input with zod
apps/server/src/router.ts
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
const appRouter = router({
greetUser: publicProcedure
.input(
z.object({
name: z.string(),
}),
)
.query((opts) => {
const { input } = opts;
return {
message: `Hello, ${input.name}!`,
};
}),
});
// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof appRouter;
  1. Serve the API
apps/server/src/1index.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "./router";
import { sharedString } from "@repo/utils";
import { corsifyResponse } from "./cors";
export default {
async fetch(request, env, ctx): Promise<Response> {
if (request.url.includes("trpc")) {
return corsifyResponse(
await fetchRequestHandler({
endpoint: "/trpc",
req: request,
router: appRouter,
}),
request,
env,
);
}
return corsifyResponse(
new Response(
JSON.stringify({
message: "Hello from Cloudflare Worker!",
shared: sharedString,
}),
{
headers: {
"Content-Type": "application/json",
},
},
),
request,
env,
);
},
} satisfies ExportedHandler<Env>;

Now the cloudflare worker should serve the trpc routes under /trpc.

Now let’s setup TRPC in the client

  1. Installing packages
apps/client
pnpm add @trpc/client@next
  1. Create a client instance
apps/client/src/trpc.ts
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "../../server/src/router";
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: "http://localhost:3000",
}),
],
});
export default trpc;
  1. Consume the API
apps/client/src/App.tsx
import { useEffect, useState } from "react";
import "./App.css";
import { sharedString } from "@repo/utils";
import trpc from "./trpc";
function App() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("");
useEffect(() => {
fetch("http://localhost:8787/")
.then((res) => res.json())
.then((data) => {
setMessage(JSON.stringify(data));
});
const user = trpc.greetUser.query({
name: "John",
});
user.then((data) => {
setMessage(data.message);
});
}, []);
return (
<div>
<h1>Vite + React + CF Workers</h1>
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<h3>The response from CF worker is: </h3>
<p>{message}</p>
<p>Shared string : {sharedString}</p>
</div>
);
}
export default App;

🎉 Congratulations, you have succesfully implemented end-to-end typesafety using TRPC


Keep in mind that this is just the setup, and you can do a lot more with TRPC. I suggest you go through the following resources to learn more:

  1. Defining Procedures
  2. Tanstack Query Integration
  3. Some Video Tutorials

If you want to see the complete code, you can find it here .

I hope you found this article helpful. If you have any questions or feedback, feel free to reach out to me.