www/docs/main/quickstart.mdx
import CodeBlock from '@theme/CodeBlock'; import TabItem from '@theme/TabItem'; import Tabs from '@theme/Tabs';
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
import { z } from "zod";
import { publicProcedure, router } from "./trpc";
type User = { id: string; name: string };
export const appRouter = router({
userList: publicProcedure
.query(async () => {
const users: User[] = [{ id: '1', name: 'Katt' }];
return users;
}),
userById: publicProcedure
.input(z.string())
.query(async (opts) => {
const { input } = opts;
const user: User = { id: input, name: 'Katt' };
return user;
}),
userCreate: publicProcedure
.input(z.object({ name: z.string() }))
.mutation(async (opts) => {
const { input } = opts;
const user: User = { id: '1', ...input };
return user;
}),
});
export type AppRouter = typeof appRouter;
import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { appRouter } from "./appRouter";
const server = createHTTPServer({
router: appRouter,
});
server.listen(3000);
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, check out usage with React and usage with Next.js.
:::info Requirements
"strict": true in your tsconfig.json as we don't officially support non-strict mode.
:::Start off by installing the @trpc/server and @trpc/client packages:
import { InstallSnippet } from '@site/src/components/InstallSnippet';
<InstallSnippet pkgs="@trpc/server @trpc/client" />:::tip AI Agents If you use an AI coding agent, install tRPC skills for better code generation:
npx @tanstack/intent@latest install
:::
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:
type User = { id: string; name: string; };
userList: () => User[];
userById: (id: string) => User;
userCreate: (data: { name: string }) => User;
Here's the file structure we'll be building. We recommend separating tRPC initialization, router definition, and server setup into distinct files to prevent cyclic dependencies:
.
├── server/
│ ├── trpc.ts # tRPC instantiation & setup
│ ├── appRouter.ts # Your API logic and type export
│ └── index.ts # HTTP server
└── client/
└── index.ts # tRPC client
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.
import { initTRPC } from '@trpc/server';
/**
* Initialization of tRPC backend
* Should be done only once per backend!
*/
const t = initTRPC.create();
/**
* Export reusable router and procedure helpers
* that can be used throughout the router
*/
export const router = t.router;
export const publicProcedure = t.procedure;
Next, we'll initialize our main router instance, commonly referred to as appRouter, to which we'll later add procedures. Lastly, we need to export the type of the router which we'll later use on the client side.
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
// ---cut---
import { router } from './trpc';
export const appRouter = router({
// ...
});
export type AppRouter = typeof appRouter;
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:
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
type User = { id: string; name: string };
// ---cut---
import { publicProcedure, router } from './trpc';
export const appRouter = router({
userList: publicProcedure
.query(async () => {
const users: User[] = [{ id: '1', name: 'Katt' }];
return users;
}),
});
export type AppRouter = typeof appRouter;
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:
:::info
Throughout the remainder of this documentation, we will use zod as our validation library.
:::
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
type User = { id: string; name: string };
// ---cut---
import { publicProcedure, router } from './trpc';
export const appRouter = 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 (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(async (opts) => {
const { input } = opts;
// ^?
const user: User = { id: input, name: 'Katt' };
return user;
}),
});
export type AppRouter = typeof appRouter;
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
type User = { id: string; name: string };
// ---cut---
import { publicProcedure, router } from './trpc';
import { z } from 'zod';
export const appRouter = router({
// ...
userById: publicProcedure
.input(z.string())
.query(async (opts) => {
const { input } = opts;
// ^?
const user: User = { id: input, name: 'Katt' };
return user;
}),
});
export type AppRouter = typeof appRouter;
:::info
Throughout the remainder of this documentation, we will use zod as our validation library.
:::
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
type User = { id: string; name: string };
// ---cut---
import { publicProcedure, router } from './trpc';
import * as yup from 'yup';
export const appRouter = router({
// ...
userById: publicProcedure
.input(yup.string().required())
.query(async (opts) => {
const { input } = opts;
// ^?
const user: User = { id: input, name: 'Katt' };
return user;
}),
});
export type AppRouter = typeof appRouter;
:::info
Throughout the remainder of this documentation, we will use zod as our validation library.
:::
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
type User = { id: string; name: string };
// ---cut---
import { publicProcedure, router } from './trpc';
import * as v from 'valibot';
export const appRouter = router({
// ...
userById: publicProcedure
.input(v.string())
.query(async (opts) => {
const { input } = opts;
// ^?
const user: User = { id: input, name: 'Katt' };
return user;
}),
});
export type AppRouter = typeof appRouter;
Similar to GraphQL, tRPC makes a distinction between Query and Mutation procedures.
The distinction between a Query and a Mutation is primarily semantic. Queries use HTTP GET and are intended for read operations, while Mutations use HTTP POST and are intended for operations that cause side effects.
Let's add a userCreate mutation by adding it as a new property on our router object:
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
import { z } from 'zod';
type User = { id: string; name: string };
// ---cut---
import { publicProcedure, router } from './trpc';
export const appRouter = router({
// ...
userCreate: publicProcedure
.input(z.object({ name: z.string() }))
.mutation(async (opts) => {
const { input } = opts;
// ^?
// Create the user in your DB
const user: User = { id: '1', ...input };
return user;
}),
});
export type AppRouter = typeof appRouter;
Now that we have defined our router, we can serve it. tRPC has first-class adapters for many popular web servers. To keep it simple, we'll use the standalone Node.js adapter here.
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
// @include: appRouter
// @filename: server.ts
// ---cut---
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { appRouter } from './appRouter';
const server = createHTTPServer({
router: appRouter,
});
server.listen(3000);
// @include: trpc
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
// ---cut---
// @include: appRouter
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
// @include: appRouter
// @filename: server.ts
// ---cut---
// @include: server
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.
// @target: esnext
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
// @include: appRouter
// @filename: client.ts
// ---cut---
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './appRouter';
// 👆 **type-only** imports are stripped at build time
// Pass AppRouter as a type parameter. 👇 This lets `trpc` know
// what procedures are available on the server and their input/output types.
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000',
}),
],
});
Links in tRPC are similar to links in GraphQL, they let us control the data flow 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.
You now have access to your API procedures on the trpc object. Try it out!
// @target: esnext
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
// @include: appRouter
// @filename: client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './appRouter';
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000',
}),
],
});
// ---cut---
// Inferred types
const user = await trpc.userById.query('1');
// ^?
const createdUser = await trpc.userCreate.mutate({ name: 'Katt' });
// ^?
You can also use your autocomplete to explore the API on your client
// @target: esnext
// @filename: trpc.ts
// @include: trpc
// @filename: appRouter.ts
// @include: appRouter
// @filename: client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './appRouter';
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000',
}),
],
});
// ---cut---
// @errors: 2339
trpc.u;
// ^|
| What's next? | Description |
|---|---|
| Example Apps | Explore tRPC in your chosen framework |
| TanStack React Query | Recommended React integration via @trpc/tanstack-react-query |
| Next.js | Usage with Next.js |
| Server Adapters | Express, Fastify, and more |
| Transformers | Use superjson to retain complex types like Date |