tRPC allows developers to create fully type safe APIs with TypeScript in full-stack applications. While the server application produces a type safe router with type safe functions (e.g. CRUD operations: create user, get user by identifier, get all users), the client application can call these functions directly on the inferred type safe router. Under the hood, HTTP is still used to communicate between client and server.
tRPC requires having TypeScript on the client and server. Since the client has to import the type safe router from the server, it kinda makes sense to have both applications in shared environment (from folder to Monorepo everything is possible). Due to the router being the glue between client and server, we get fully typed APIs without any schemas (e.g. REST with OpenAPI ) or code generation (e.g. GraphQL with GraphQL Code Generator ).
While GraphQL and REST can both create type safe APIs, they always need an intermediate step to create these types in an extra generated file. For example, GraphQL Code Generator creates a new type safe schema file. With tRPC you get type safe functions for your client application that put simply run the code on the server.
Compared to GraphQL and REST, tRPC is mainly used (so far) for smaller projects where not many services need to be orchestrated (e.g. GraphQL) or we do not work necessarily on resources with a standardized RESTful approach. However, itâs possible to migrate any time from tRPC to GraphQL/REST, because tRPC in the end is just functions on the server which can be directly used in a REST router or GraphQL resolvers.
The following tutorial is a walkthrough of creating a smallish CRUD application with Node + Express on the server and React on the client. We will use tRPC to establish the communication between both worlds. The shown folder structure will be partly created in the next steps, but you can start with creating the root folder:
In this section of the tutorial, weâll create the server application with Node, tRPC and Express. First, create a new folder for the server in your project on the command line:
Next, create a minimal JavaScript project and upgrade it to a TypeScript project:
After you have a running TypeScript application, we can start with the actual tRPC implementation for the server which will expose a fully type safe API to the client eventually. First, we will install the tRPC server dependencies on the command line:
Second, we will also install Zod for having a type safe schema validation. For example, Zod will allow us to validate the input from a user that reaches the API on the server:
Now an overview of what we are building from a birdâs eye perspective:
You can create all the folders/files along the way. As you can see, the server application will have a user/ folder. There we will implement all the CRUD operations (e.g. create a user) for the user domain. Letâs start with the User type definition in the src/user/types.ts file. First each user will only have an id and a name :
Second, we will fill a pseudo database file called src/user/db.ts with two users:
And third, we will create the core of this tutorial where we will implement the CRUD API with tRPC for the user domain in the src/user/router.ts file. Weâll create functions for getUsers , getUserById and createUser with a query (read) or mutation (write). Feel free to add functions for updateUserById and deleteUserById yourself:
The last file creates a userRouter which exposes all the user related functions to the client application eventually. This domain specific router will be used in a root router which aggregates all domain specific routers of the application later.
Furthermore, we are using the User type definition and the pseudo database which we have created earlier. When we look closer at the createUser function and its chained input function, we can see how Zod is used to validate the input as name of the data type string. Even though Zod is perfect for schema validations in TypeScript projects, we can use any validation (e.g. Yup ) in tRPC.
Next, we need to implement the actual tRPC router and the publicProcedure which we already used in the previous userRouter . Create a new src/trpc.ts file and add the following implementation:
Essentially we created the foundation for a type safe router and a public procedure without any restrictions. This is kinda the default tRPC setup. It would also be the place where you could add protected procedures (e.g. authentication) with middlewares (e.g. checking the authenticated user) eventually. Whatâs missing is the src/context.ts file:
Again, a basic foundation for the context which is used for every API call. However, here we already specified that we want to use Express with tRPC. Other adapters like Fastify are available too. We did not pass anything in the returned context yet, but this would be the place to add the authenticated user when there is a valid session token coming from the request. Then a middleware from the previous file could check the authenticated user for protected procedures.
Earlier we created a user specific router with tRPC. Next we will create the root router in a new src/router.ts file which consolidates all domain specific routers. Here you may add other domain specific routers eventually:
Last, we need to setup the actual server. We will be using Express here, because we already used the tRPC + Express adapter for the context, however, there are other options available. Letâs install Express first:
And second, use it together with the router and context that we created earlier in a top-level src/index.ts file:
The implementation is not much different from a standalone Express server. The only two differences is the Express aware integration of tRPC. Furthermore, we export the type AppRouter which will be used by the client application eventually. Note that the AppRouter is not the implementation of the router, but only its type definitions.
Start your server with npm start and verify that there are no errors showing up. Next we will continue with the client application which will make use of the exported AppRouter type definitions and its typed functions (e.g. createUser ).
Disclaimer: The frontend application in React will be using Vite instead of Next.js, because I usually teach client-side routing over server-side routing for beginners in my books like The Road to React as well. For readers of my content, this will be the perfect continuing content after learning the fundamentals of frontend and backend development with React and Node.
We will start by using Vite for creating a React with TypeScript frontend application. On the command line, move into the projectâs folder. There create a client/ folder with the following instructions on the command line (next to the server/ folder):
Vite takes care of creating a minimal frontend application with React and TypeScript. After you have entered the last command, you should see a folder/file structure in the client/ folder. Next, move into the new client/ folder, install all dependencies, and start the application. You should find a barebones web application in the browser:
From here we will start the tRPC specific implementation for this React/TypeScript application. Therefore, you have to install these two dependencies to your frontend project. First, the tRPC client which helps us to make requests to the tRPC server. And second, the tRPC server, which we will not use directly as dependency in the frontend, but which is a necessary peer dependency of the tRPC client:
The ultimate goal here is creating an end-to-end type safety with TypeScript throughout the full stack of frontend and backend application. Therefore we will create a file called src/trpc.ts where this magic happens:
Essentially we are instantiating the tRPC client here â which takes as a bare minimum of an URL for the API of the server application. However, the magic lies in the imported AppRouter type which gets imported from the server/ project into the client/ project. Next the AppRouter type can be used as TypeScript generic for creating the client-side tRPC instance. As result, we inherit all the types from the backend in the frontend. In other words, we achieved end-to-end type safety with TypeScript.
We will prove the previous statement in a React component now. The following implementation shows how the src/App.tsx file imports the tRPC client instance from the previous file. It gets used in Reactâs useEffect Hook where it fetches an actual user from the server when the component renders. Whatâs important is that everything on the trpc client is type safe and therefore can be auto completed (e.g. user and getUserById ) in your IDE (e.g. VSCode):
When you open the browser, you should see the user getting logged over there. Nothing gets rendered though due to the use of React fragments. If you cannot see the logging and see an error instead, make sure to start your server/ project on the command line, because otherwise itâs not available for the client application.
When it comes to data fetching in React, one cannot get around React Query , because it comes with powerful features like caching, refetching, and retries on failure. But it already starts with a loading state which we donât have to manage ourselves all the time. Fortunately, tRPC comes with a React Query integration that we will set up next. First, install React Query (RQ) and tRPCâs React Query integration for it:
Next go into your src/trpc.ts file and include the new tRPC to React Query adapter there. Do not forget to export both tRPC instances from this file:
In your src/main.tsx file, provide both React Query and tRPC globally:
Finally you can use tRPC with React Query. On the tRPC instance, you can access the user router, from there the getUserById procedure, and finally from there React Queryâs useQuery Hook. The returned result is native to React Query and fully typed. You can access all the properties on trpc and data with your IDEâs auto complete:
Changing the method from getUserById to getUsers yields an immediate type error:
We can fix this with an appropriate implementation which renders a list of users:
We have used the getUserById and getUsers functions. Last, we will use the createUser function that we defined on the serverâs router to create a user from the UI. We will implement a form in React which enables a user to input a name and to send the request. See again how the tRPC instance enables us to access the user domain and its functions (here getUsers and createUser ) and their respective query/mutation functions which are native to React Query and fully typed:
Thatâs it. We have implemented a server and client application with tRPC. While the server uses Express, the client uses React and React Query. We implemented only a few type safe API endpoints here, however, you can imagine how this scales with having more domain specific routers (e.g. Post, Comment) and more query/mutation functions within the router (e.g. deleteUserById ).
tRPC is a great solution for full-stack type safe applications which are using TypeScript on both client and server and which share a codebase. It can be the perfect fit for bootstrapping a new project, because once the barebones are set up, it offers an incredible developer experience for scaling a type safe API. Later, only if needed, one can migrate to GraphQL or REST and reuse the functions over there.