Quickstart
tRPC combines concepts from REST and GraphQL. If you are unfamiliar with either, take a look at the key Concepts.
Installation
tRPC is split between several packages, so you can install only what you need. Make sure to install the packages you want in the proper sections of your codebase. For this quickstart guide we'll keep it simple and use the vanilla client only. For framework guides, checkout usage with React and usage with Next.js.
- tRPC requires TypeScript >= 4.7.0
- We strongly recommend you using
"strict": truein yourtsconfig.jsonas we don't officially support non-strict mode.
Start off by installing the @trpc/server and @trpc/client packages:
- npm
- yarn
- pnpm
- bun
shnpm install @trpc/server @trpc/client
shnpm install @trpc/server @trpc/client
shyarn add @trpc/server @trpc/client
shyarn add @trpc/server @trpc/client
shpnpm add @trpc/server @trpc/client
shpnpm add @trpc/server @trpc/client
shbun add @trpc/server @trpc/client
shbun add @trpc/server @trpc/client
Defining a backend router
Let's walk through the steps of building a typesafe API with tRPC. To start, this API will contain three endpoints with these TypeScript signatures:
tstype User = { id: string; name: string; };userList: () => User[];userById: (id: string) => User;userCreate: (data: { name: string }) => User;
tstype User = { id: string; name: string; };userList: () => User[];userById: (id: string) => User;userCreate: (data: { name: string }) => User;
1. Create a router instance
First, let's initialize the tRPC backend. It's good convention to do this in a separate file and export reusable helper functions instead of the entire tRPC object.
server/trpc.tstsimport {initTRPC } from '@trpc/server';/*** Initialization of tRPC backend* Should be done only once per backend!*/constt =initTRPC .create ();/*** Export reusable router and procedure helpers* that can be used throughout the router*/export constrouter =t .router ;export constpublicProcedure =t .procedure ;
server/trpc.tstsimport {initTRPC } from '@trpc/server';/*** Initialization of tRPC backend* Should be done only once per backend!*/constt =initTRPC .create ();/*** Export reusable router and procedure helpers* that can be used throughout the router*/export constrouter =t .router ;export constpublicProcedure =t .procedure ;
Next, we'll initialize our main router instance, commonly referred to as appRouter, in which we'll later add procedures to. Lastly, we need to export the type of the router which we'll later use on the client side.
server/index.tstsimport {router } from './trpc';constappRouter =router ({// ...});// Export type router type signature,// NOT the router itself.export typeAppRouter = typeofappRouter ;
server/index.tstsimport {router } from './trpc';constappRouter =router ({// ...});// Export type router type signature,// NOT the router itself.export typeAppRouter = typeofappRouter ;
2. Add a query procedure
Use publicProcedure.query() to add a query procedure to the router.
The following creates a query procedure called userList that returns a list of users from our database:
server/index.tstsimport {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({userList :publicProcedure .query (async () => {// Retrieve users from a datasource, this is an imaginary databaseconstusers = awaitdb .user .findMany ();returnusers ;}),});
server/index.tstsimport {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({userList :publicProcedure .query (async () => {// Retrieve users from a datasource, this is an imaginary databaseconstusers = awaitdb .user .findMany ();returnusers ;}),});
3. Using input parser to validate procedure inputs
To implement the userById procedure, we need to accept input from the client. tRPC lets you define input parsers to validate and parse the input. You can define your own input parser or use a validation library of your choice, like zod, yup, or superstruct.
You define your input parser on publicProcedure.input(), which can then be accessed on the resolver function as shown below:
- Vanilla
- Zod
- Yup
- Valibot
server/index.tstsconstappRouter =router ({// ...userById :publicProcedure // The input is unknown at this time. A client could have sent// us anything so we won't assume a certain data type..input ((val : unknown) => {// If the value is of type string, return it.// It will now be inferred as a string.if (typeofval === 'string') returnval ;// Uh oh, looks like that input wasn't a string.// We will throw an error instead of running the procedure.throw newError (`Invalid input: ${typeofval }`);}).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server/index.tstsconstappRouter =router ({// ...userById :publicProcedure // The input is unknown at this time. A client could have sent// us anything so we won't assume a certain data type..input ((val : unknown) => {// If the value is of type string, return it.// It will now be inferred as a string.if (typeofval === 'string') returnval ;// Uh oh, looks like that input wasn't a string.// We will throw an error instead of running the procedure.throw newError (`Invalid input: ${typeofval }`);}).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
ZodType, e.g. z.string() or z.object().server.tstsimport {z } from 'zod';import {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({// ...userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server.tstsimport {z } from 'zod';import {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({// ...userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
YupSchema, e.g. yup.string() or yup.object().server.tstsimport * asyup from 'yup';import {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({// ...userById :publicProcedure .input (yup .string ().required ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server.tstsimport * asyup from 'yup';import {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({// ...userById :publicProcedure .input (yup .string ().required ()).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
wrap your schema with [TypeSchema](https://typeschema.com).server.tstsimport {wrap } from '@decs/typeschema';import {string } from 'valibot';import {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({// ...userById :publicProcedure .input (wrap (string ())).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
server.tstsimport {wrap } from '@decs/typeschema';import {string } from 'valibot';import {db } from './db';import {publicProcedure ,router } from './trpc';constappRouter =router ({// ...userById :publicProcedure .input (wrap (string ())).query (async (opts ) => {const {input } =opts ;// Retrieve the user with the given IDconstuser = awaitdb .user .findById (input );returnuser ;}),});
Throughout the remaining of this documentation, we will use zod as our validation library.
4. Adding a mutation procedure
Similar to GraphQL, tRPC makes a distinction between query and mutation procedures.
The way a procedure works on the server doesn't change much between a query and a mutation. The method name is different, and the way that the client will use this procedure changes - but everything else is the same!
Let's add a userCreate mutation by adding it as a new property on our router object:
server.tstsconstappRouter =router ({// ...userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;// Create a new user in the databaseconstuser = awaitdb .user .create (input );returnuser ;}),});
server.tstsconstappRouter =router ({// ...userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;// Create a new user in the databaseconstuser = awaitdb .user .create (input );returnuser ;}),});
Serving the API
Now that we have defined our router, we can serve it. tRPC has many adapters so you can use any backend framework of your choice. To keep it simple, we'll use the standalone adapter.
server/index.tstsimport {createHTTPServer } from '@trpc/server/adapters/standalone';import {router } from './trpc';constappRouter =router ({// ...});constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
server/index.tstsimport {createHTTPServer } from '@trpc/server/adapters/standalone';import {router } from './trpc';constappRouter =router ({// ...});constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
See the full backend code
server/db.tststypeUser = {id : string;name : string };// Imaginary databaseconstusers :User [] = [];export constdb = {user : {findMany : async () =>users ,findById : async (id : string) =>users .find ((user ) =>user .id ===id ),create : async (data : {name : string }) => {constuser = {id :String (users .length + 1), ...data };users .push (user );returnuser ;},},};
server/db.tststypeUser = {id : string;name : string };// Imaginary databaseconstusers :User [] = [];export constdb = {user : {findMany : async () =>users ,findById : async (id : string) =>users .find ((user ) =>user .id ===id ),create : async (data : {name : string }) => {constuser = {id :String (users .length + 1), ...data };users .push (user );returnuser ;},},};
server/trpc.tstsimport {initTRPC } from '@trpc/server';constt =initTRPC .create ();export constrouter =t .router ;export constpublicProcedure =t .procedure ;
server/trpc.tstsimport {initTRPC } from '@trpc/server';constt =initTRPC .create ();export constrouter =t .router ;export constpublicProcedure =t .procedure ;
server/index.tstsimport {createHTTPServer } from "@trpc/server/adapters/standalone";import {z } from "zod";import {db } from "./db";import {publicProcedure ,router } from "./trpc";constappRouter =router ({userList :publicProcedure .query (async () => {constusers = awaitdb .user .findMany ();returnusers ;}),userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .findById (input );returnuser ;}),userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .create (input );returnuser ;}),});export typeAppRouter = typeofappRouter ;constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
server/index.tstsimport {createHTTPServer } from "@trpc/server/adapters/standalone";import {z } from "zod";import {db } from "./db";import {publicProcedure ,router } from "./trpc";constappRouter =router ({userList :publicProcedure .query (async () => {constusers = awaitdb .user .findMany ();returnusers ;}),userById :publicProcedure .input (z .string ()).query (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .findById (input );returnuser ;}),userCreate :publicProcedure .input (z .object ({name :z .string () })).mutation (async (opts ) => {const {input } =opts ;constuser = awaitdb .user .create (input );returnuser ;}),});export typeAppRouter = typeofappRouter ;constserver =createHTTPServer ({router :appRouter ,});server .listen (3000);
Using your new backend on the client
Let's now move to the client-side code and embrace the power of end-to-end typesafety. When we import the AppRouter type for the client to use, we have achieved full typesafety for our system without leaking any implementation details to the client.
1. Setup the tRPC Client
client/index.tstsimport {createTRPCProxyClient ,httpBatchLink } from '@trpc/client';import type {AppRouter } from './server';// 👆 **type-only** import// Pass AppRouter as generic here. 👇 This lets the `trpc` object know// what procedures are available on the server and their input/output types.consttrpc =createTRPCProxyClient <AppRouter >({links : [httpBatchLink ({url : 'http://localhost:3000',}),],});
client/index.tstsimport {createTRPCProxyClient ,httpBatchLink } from '@trpc/client';import type {AppRouter } from './server';// 👆 **type-only** import// Pass AppRouter as generic here. 👇 This lets the `trpc` object know// what procedures are available on the server and their input/output types.consttrpc =createTRPCProxyClient <AppRouter >({links : [httpBatchLink ({url : 'http://localhost:3000',}),],});
Links in tRPC are similar to links in GraphQL, they let us control the data flow before being sent to the server. In the example above, we use the httpBatchLink, which automatically batches up multiple calls into a single HTTP request. For more in-depth usage of links, see the links documentation.
2. Querying & mutating
You now have access to your API procedures on the trpc object. Try it out!
client/index.tsts// Inferred typesconstuser = awaittrpc .userById .query ('1');constcreatedUser = awaittrpc .userCreate .mutate ({name : 'sachinraja' });
client/index.tsts// Inferred typesconstuser = awaittrpc .userById .query ('1');constcreatedUser = awaittrpc .userCreate .mutate ({name : 'sachinraja' });
Full autocompletion
You can open up your Intellisense to explore your API on your frontend. You'll find all of your procedure routes waiting for you along with the methods for calling them.
client/index.tsts// Full autocompletion on your routestrpc .u ;
client/index.tsts// Full autocompletion on your routestrpc .u ;
Try it out for yourself!
Next steps
We highly encourage you to check out the example apps to learn about how tRPC is installed in your favorite framework.
By default, tRPC will map complex types like Date to their JSON-equivalent (string in the case of Date). If you want to add to retain the integrity of those types, the easiest way to add support for these is to use superjson as a Data Transformer.
tRPC includes more sophisticated client-side tooling designed for React projects and Next.js.