Should You Trust Your API with tRPC

Before We Get Started

This post was originally written for the Bitovi blog you can find the original post here

How much should you trust your APIs? Trust between the frontend and backend is fundamental—in most modern web applications, both sides are ultimately accountable for the application.

However, sometimes something goes wrong, and things don’t work as intended. Fields are left off responses, causing crashes and errors. Documentation goes un-updated, resulting in versioning issues.

Are frontend/backend errors inevitable? At Bitovi, we constantly evaluate new technologies to ensure we provide our clients with modern solutions. One such solution we’ve been experimenting with is tRPC.

What is tRPC?

tRPC allows you to quickly build and consume fully typesafe APIs without schemas or code generation. Using tRPC requires both your front and backend to be written in TypeScript.

On the backend side, tRPC does all the TypeScript magic to allow the backend team to define their queries and mutations and create a single robust type.

On the frontend side, tRPC provides a factory that consumes the single type from the backend and creates React Query hooks for mutations and queries.

Why Use tPRC?

tRPC requires your team to use TypeScript on both the front and backend of your project. TypeScript provides static typing and increases the readability of your code. Additionally, TypeScript’s integration into IDEs makes for a streamlined developer experience. However, if your backend supports multiple platforms, such as mobile, they will need to be written in TypeScript to leverage tRPC fully. Depending on the composition of your team, this could be a non-starter.

tRPC allows you to create a full-stack application VERY quickly, which is ideal if you have a small team and need to get a project up and off the ground.

Something to be aware of is tRPC does not provide any mechanism for sharing the types across both ends of the application. This allows developers to implement their own solutions, which generally means using a monorepo for the project.

tRPC leans heavily on TypeScript’s type inference. In a smaller project, the heavy usage of type inference allows developers to move quickly. However, as the project grows and becomes more mature, there is the possibility of running into TypeScript performance problems, which are notoriously hard to fix and greatly detriment developer experience.

Currently, tRPC is a relatively young project and doesn’t support every library and framework. Below is the current list of supported libraries and frameworks.

  • Backend

    • Express
    • Fastify
    • AWS Lambda
    • The alpha version also supports the fetch API but hasn’t been released yet
  • Frontend

    • React
    • Next

They are currently looking for contributors for other popular frontend frameworks and libraries like Svelte and Vue.

The Demo Application

This post is meant to be more about sharing opinions rather than a how-to or tutorial, so we won’t be getting into any application code, bar a few examples. Since we won't be looking at the code for the demo application, let me provide some context around what was built.

I used tRPC’s v10 API, which is now in beta. I prefer the syntax for defining procedures in the v10 API, and it is also the direction tRPC has chosen to go. I built a simple CRUD application that used React and Express. These two libraries are the technologies that I am most familiar with that tRPC supports.

// old procdedure api
export const appRouter = trpc
// Create procedure at path 'hello'
.query("hello", {
resolve({ ctx }) {
return {
greeting: `hello world`,
// new procedure api
export const appRouter = t.router({
// Create procedure at path 'hello'
hello: t.query(() => {
return {
greeting: "hello world",


Setting up queries and mutations on the backend side is straightforward because the procedure API is consistent. You can do two things—query and mutate—which are technically the same thing; the only difference is semantics. Like queries and mutations, tPRC also provides an intuitive API for subscriptions.

Within their procedure API, developers can smoothly integrate popular data validation libraries like zod or yup to provide static and runtime type validation.

// partial procedure showing zod validation
deleteMovie: t.procedure.input(z.string().uuid()).query(() => {
// Rest of query

Similar to popular node frameworks, tRPC provides a straightforward way to compose multiple routers, making it easy to scale up and add more procedures for different things without additional bloat. I found the brownfield integration to be misleading in the documentation.

Does it integrate into existing applications easily? Yes, and it's easy to add new endpoints; however, if you want to use any preexisting endpoints, you must redefine them using tRPC’s procedures.


On the frontend, tRPC creates most of your service layer in a single function call.

// service layer factory call
// Below is the shared type
import type { AppRouter } from "@trpc-tutorial/trpc-routers"
import { createReactQueryHooks } from "@trpc/react"
// creates all of the service layer
export const trpc = createReactQueryHooks<AppRouter>()

tPRC wraps React Query and provides intuitive typing for query keys and mutations. React Query supplies everything you need to manage any kind of async/server state on your frontend. React Query uses SWR (stale while revalidating) as its caching mechanism, allowing your applications to feel faster by reducing the number of times a loading indicator appears on the screen.

The tRPC wrapper around the query parts of React Query is fantastic. tRPC removes the need for creating a client to talk to the backend, so once you’ve set up the providers, you can use the hooks and have all the types and data you need. Additionally, you get to see the end-to-end type safety immediately after you start using the hooks. tRPC uses the shared type from the backend to provide easy ways of looking up your query and mutation keys.

Being a big fan of React Query, I noticed a couple of shortcomings with the tRPC/React Query integration. The first is that there is no typing around query invalidation.

In React Query, you call a function invalidating keys where that query’s data is cached to re-trigger a query after a mutation has been performed. The keys are created by tRPC, and when you create queries, you get full support from IntelliSense and TypeScript; however, you don’t get that for query invalidation.

The other shortcoming is setting up the providers. Using tRPC requires that you also set up React Query, which isn’t challenging and only needs to be done once, but it feels like it should be done for you by tRPC.

// Example setup of trpc and react query providers
import React, { useState } from "react"
import { QueryClient, QueryClientProvider } from "react-query"
import { trpc } from "./trpc"
const client = new QueryClient()
export const TRPCProvider = ({ children }: { children: React.ReactNode }) => {
const [trpcClient] = useState(() =>
url: "api/trpc",
return (
<trpc.Provider client={trpcClient} queryClient={client}>
<QueryClientProvider client={client}>{children}</QueryClientProvider>

Concluding Thoughts

We initially began exploring tRPC to find out whether or not it helps solve the problem of trusting APIs and, in general, see if it can improve communication between the front and backend. Which it does. However, this doesn’t mean tRPC is the right choice for every situation.

tPRC is a great choice if you’re on a small team working on a green field project that needs to get up and running quickly. It’ll allow developers to quickly transition between both ends of the stack to implement features.

tRPC may not be the best choice if you are working on an established project, especially one not already in a monorepo. While it may be a viable option, it may require more effort to add and implement than it is worth.

Many things could impact people's experiences with tRPC; these are my current opinions, having used it to build a sample project. Have you used tRPC and had a different experience with it that you’d like to share? We would love to hear from you! Bitovi always welcomes new viewpoints to spur further discussion and thought.