Skip to main content
Version: 10.x

Quickstart

tip

We highly encourage you to check out the example apps to learn about how tRPC is installed in your favorite framework.

Installation

tRPC is broken up into 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.

Requirements
  • tRPC requires TypeScript >= 4.7.0
  • We strongly recommend you using "strict": true in your tsconfig.json as we don't officially support non-strict mode.
PurposeLocationInstall command
Implement endpoints and routersServernpm install @trpc/server
Call procedures on clientClientnpm install @trpc/client @trpc/server
React hooks powered by @tanstack/react-query Clientnpm install @trpc/react-query @tanstack/react-query
Next.js integration utilities Next.jsnpm install @trpc/next

Installation Snippets

Here are some install scripts to add tRPC to your project. These scripts include every @trpc/* package, so feel free to remove what you don't need!

sh
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query
sh
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query

Defining a backend router

Let's walk through the steps of building a typesafe API with tRPC. To start, this API will only contain two endpoints with these TypeScript signatures:

ts
userById(id: string) => { id: string; name: string; }
userCreate(data: { name: string }) => { id: string; name: string; }
ts
userById(id: string) => { id: string; name: string; }
userCreate(data: { name: string }) => { id: string; name: string; }

Create a router instance

First, let's define an empty router in our server codebase:

server.ts
ts
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;
server.ts
ts
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;

Add a query procedure

Use procedure.query() to add a query procedure/endpoint to the router. The two methods on this procedure are:

  • input (optional): When provided, this should be a function that validates and casts the input of this procedure. It should return a strongly typed value when the input is valid or throw an error if the input is invalid. We recommend using a TypeScript validation library like Zod, Superstruct, or Yup for input validation.
  • query: This is the implementation of the procedure (a "resolver"). It's a function with a single req argument to represent the incoming request. The validated (and strongly typed!) input is passed into req.input.

The following creates a query procedure called userById that takes a single string argument and returns a user object:

server.ts
ts
// @filename: server.ts
import { initTRPC } from '@trpc/server';
 
const t = initTRPC.create();
 
interface User {
id: string;
name: string;
}
 
const userList: User[] = [
{
id: '1',
name: 'KATT',
},
];
 
const appRouter = t.router({
userById: t.procedure
// 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.
// TypeScript now knows that this value is a string.
if (typeof val === 'string') return val;
 
// Uh oh, looks like that input wasn't a string.
// We will throw an error instead of running the procedure.
throw new Error(`Invalid input: ${typeof val}`);
})
.query((req) => {
const { input } = req;
const input: string
const user = userList.find((u) => u.id === input);
 
return user;
}),
});
 
export type AppRouter = typeof appRouter;
server.ts
ts
// @filename: server.ts
import { initTRPC } from '@trpc/server';
 
const t = initTRPC.create();
 
interface User {
id: string;
name: string;
}
 
const userList: User[] = [
{
id: '1',
name: 'KATT',
},
];
 
const appRouter = t.router({
userById: t.procedure
// 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.
// TypeScript now knows that this value is a string.
if (typeof val === 'string') return val;
 
// Uh oh, looks like that input wasn't a string.
// We will throw an error instead of running the procedure.
throw new Error(`Invalid input: ${typeof val}`);
})
.query((req) => {
const { input } = req;
const input: string
const user = userList.find((u) => u.id === input);
 
return user;
}),
});
 
export type AppRouter = typeof appRouter;

Add 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.ts
ts
// @filename: server.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const t = initTRPC.create();
 
const router = t.router;
const publicProcedure = t.procedure;
 
interface User {
id: string;
name: string;
}
 
const userList: User[] = [
{
id: '1',
name: 'KATT',
},
];
 
const appRouter = router({
userById: publicProcedure
.input((val: unknown) => {
if (typeof val === 'string') return val;
throw new Error(`Invalid input: ${typeof val}`);
})
.query((req) => {
const input = req.input;
const user = userList.find((it) => it.id === input);
 
return user;
}),
userCreate: publicProcedure
.input(z.object({ name: z.string() }))
.mutation((req) => {
const id = `${Math.random()}`;
 
const user: User = {
id,
name: req.input.name,
};
 
userList.push(user);
 
return user;
}),
});
 
export type AppRouter = typeof appRouter;
server.ts
ts
// @filename: server.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
 
const t = initTRPC.create();
 
const router = t.router;
const publicProcedure = t.procedure;
 
interface User {
id: string;
name: string;
}
 
const userList: User[] = [
{
id: '1',
name: 'KATT',
},
];
 
const appRouter = router({
userById: publicProcedure
.input((val: unknown) => {
if (typeof val === 'string') return val;
throw new Error(`Invalid input: ${typeof val}`);
})
.query((req) => {
const input = req.input;
const user = userList.find((it) => it.id === input);
 
return user;
}),
userCreate: publicProcedure
.input(z.object({ name: z.string() }))
.mutation((req) => {
const id = `${Math.random()}`;
 
const user: User = {
id,
name: req.input.name,
};
 
userList.push(user);
 
return user;
}),
});
 
export type AppRouter = typeof appRouter;

Using your new backend on the client

Let's now move to your frontend 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.

Setup the tRPC Client

client.ts
ts
// @filename: client.ts
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
 
// Notice the <AppRouter> generic here.
const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
}),
],
});
client.ts
ts
// @filename: client.ts
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
 
// Notice the <AppRouter> generic here.
const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc',
}),
],
});

Querying & mutating

You now have access to your API procedures on the trpc object. Try it out!

client.ts
ts
// Inferred types
const user = await trpc.userById.query('1');
user.id;
(property) id: string
user.name;
(property) name: string
 
const createdUser = await trpc.userCreate.mutate({ name: 'sachinraja' });
createdUser.id;
(property) id: string
createdUser.name;
(property) name: string
client.ts
ts
// Inferred types
const user = await trpc.userById.query('1');
user.id;
(property) id: string
user.name;
(property) name: string
 
const createdUser = await trpc.userCreate.mutate({ name: 'sachinraja' });
createdUser.id;
(property) id: string
createdUser.name;
(property) name: string

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.ts
ts
// Full autocompletion on your routes
trpc.u;
      
client.ts
ts
// Full autocompletion on your routes
trpc.u;
      

Next steps

tip

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.