Telerik blogs

Learn about tRPC and how it can help us remotely call backend functions on the client side using TypeScript inference for working fast and efficiently with API.

When we start on our path to learning programming, the first thing we notice is that there are a lot of terms and abbreviations. It seems like a bunch of words are randomly put together, and inside our heads, we start to think: “What is this, and why should I use it?”

One of the things that can be the hardest to learn is what APIs are and how to build them. Every developer has to search for many ways to make reliable and scalable APIs. It is part of our lives, whether we like it or not. Every application has some API that makes things work under the hood.

APIs can be challenging to build for several reasons. One of the most important reasons is the speed at which software development changes over time. Libraries and frameworks used today can become unreliable for many reasons in a short period. Documentation is another pain point when we talk about APIs; documentation is essential to help us understand how the API works and the correct ways to use it.

Especially when we’re using TypeScript for building APIs, it is hard to maintain conciseness between the API and the web application that is using it. Sometimes, we must change something on the API that could break our application.

Today, we will cover tRPC and how it can help us to move fast and break nothing using TypeScript. It is a lightweight library for remotely calling backend functions on the client side. It allows us to use TypeScript inference to make communication between the backend and frontend more productive.

tRPC—Move Fast and Break Nothing

tRPC stands for TypeScript Remote Procedure Call, and it allows developers to build high-performance APIs quickly. It requires both your frontend and backend to be written in TypeScript to make communication more productive.

There are many reasons why tPRC is so great for building reliable APIs, such as:

  • Automatic type-safety — It helps to maintain the communication between the frontend and backend, telling us when something has changed on the server and warning us of errors.
  • Framework agnostic — We can make use of tRPC no matter what framework we’re using, as long as we use TypeScript on both ends (frontend and backend).
  • Developer experience — We can quickly build and consume fully type-safe APIs without schemas or code generation.

How tRPC Works

The way tRPC works is straightforward: it gives you end-to-end type safety from your server to your client without declaring types, using schemas or using code generation. It was designed to be a more accessible and reliable way of building modern APIs using the Remote Procedure Call protocol and TypeScript inference.

One of the essential things in tRPC is procedures. Procedures are flexible and composable primitives to create backend functions. They use a builder pattern, which means you can use as many procedures as you want and reuse them in different parts of your server.

A procedure in tRPC looks like this:

import { router, publicProcedure } from './trpc';
import { z } from 'zod';
 
const appRouter = router({
  sayMyName: publicProcedure.query(() => {
    return {
      name: 'Leonardo Maldonado',
    };
  }),
});

There are two types of procedures: queries and mutations. If you have ever worked with GraphQL before, you might know the difference between them, but as a recap: queries are used when we want to read data. A mutation is used when we want to create, modify or delete data.

Another essential concept of tRPC is routers, which allow us to organize our API routes modularly and hierarchically. We will learn more about procedures and routers by building an example. Let’s do it.

tRPC in Practice

We will implement some simple code that contains only endpoints and routers. We won’t use any JavaScript framework to show it in action. The idea here is to show how tRPC works rather than how it works with a specific framework.

To get started, we need to have already TypeScript installed and then install the @trpc/server package:

npm install @trpc/server

Now, we’re going to create a server.ts file and put this code inside:

import { initTRPC } from '@trpc/server';
 
const t = initTRPC.create();
 
const router = t.router;
const publicProcedure = t.procedure;
 
const appRouter = router({});
 
// Export type router type signature, not the router itself.
export type AppRouter = typeof appRouter;

We’re using the initTRPC builder function to create a new tRPC object (we should have it only per backend). After that, we’re building a new appRouter configuration, which will be helpful for us in the future, when we place our routers.

Now, we’re going to create our first procedure. Our first procedure will be named sayMyName, in which we will pass a string as an argument, and it will return a message with our name. This is how it should look:

const appRouter = router({
  sayMyName: t.procedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => {
      const { name } = input;
      return `Hello {name}, nice to see that you're learning tRPC!`;
    })
});

As we can see, the name of our first procedure is sayMyName, and it receives a name as an argument. We’re using an input validation to ensure that we receive a string as an argument, and an excellent package to help us with that is Zod. It makes it easy to add validation to our code, and it works out of the box with tRPC, so a good tip is whenever you use tRPC, try to use Zod, too, because it makes everything easier.

Now that we have created our first query, let’s make our first mutation. We will develop a mutation called addUser, which we will use to add a new user to an array called users. Let’s create our type for an user and the array:

type User = {
  name: string;
};

const users: Array<User> = [];

Let’s now create our addUser mutation. It is pretty similar to our query, but instead of returning a string, we’re going to push our new user to the users’ array and return the array like this:

const appRouter = router({
  sayMyName: t.procedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => {
      const { name } = input;
      return `Hello {name}, nice to see that you're learning tRPC!`;
    }),
  addUser: t.procedure
    .input(z.object({ name: z.string() }))
    .mutation(({ input }) => {
      users.push(input);
      return users;
    })
});

Conclusion

We introduced tRPC and how it can be mighty for building modern, reliable and modular APIs. Using tRPC increases the development in general because the API code and the client are very close to each other, helping to detect possible bugs when changes are made. It removes all the unnecessary code you need to create a server and run it fast. It is much quicker and easier to implement, which is one reason why the adoption of tRPC is increasing.

Use it in a small project and learn more about its powerful features and what good it can bring to the table. You won’t regret it!


Leonardo Maldonado
About the Author

Leonardo Maldonado

Leonardo is a full-stack developer, working with everything React-related, and loves to write about React and GraphQL to help developers. He also created the 33 JavaScript Concepts.

Related Posts

Comments

Comments are disabled in preview mode.