Move Fast and Break Nothing.End-to-end typesafe APIs made easy.
Experience the full power of TypeScript inference to boost productivity for your full-stack application.
$ pnpm add @trpc/server @trpc/client
Supported by
const const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
greeting: QueryProcedure<{
input: {
name: string;
};
output: `Hello ${string}`;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>>
trpc = createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
greeting: QueryProcedure<{
input: {
name: string;
};
output: `Hello ${string}`;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
greeting: QueryProcedure<{
input: {
name: string;
};
output: `Hello ${string}`;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>>): TRPCClient<...>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
greeting: QueryProcedure<{
input: {
name: string;
};
output: `Hello ${string}`;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>> & DecorateCreateRouterOptions<{
greeting: QueryProcedure<{
input: {
name: string;
};
output: `Hello ${string}`;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>
AppRouter>({
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
greeting: QueryProcedure<{
input: {
name: string;
};
output: `Hello ${string}`;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>>[]
links: [
httpBatchLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
greeting: QueryProcedure<{
input: {
name: string;
};
output: `Hello ${string}`;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>>(opts: HTTPBatchLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
httpBatchLink({
url: string | URLurl: 'http://localhost:3000',
}),
],
});
const res = await const trpc: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
greeting: QueryProcedure<{
input: {
name: string;
};
output: `Hello ${string}`;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>>
trpc.greeting: {
query: Resolver<{
input: {
name: string;
};
output: `Hello ${string}`;
errorShape: DefaultErrorShape;
transformer: false;
}>;
}
greeting.query: (input: {
name: string;
}, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<`Hello ${string}`, DefaultErrorShape>
query({ name: stringname: 'John' });The client of the router below: the types come from the server, with nothing generated.
As used by
How it works
It's quick and easy to get started with tRPC to build a typesafe API.
Define your procedures
Define proceduresThe first step to creating a tRPC API is to define your procedures.
Procedures are the functions we will use to build your backend. They're composable and can be queries, mutations, or subscriptions. Routers contain multiple procedures.
In this procedure, we use a Zod validator to ensure the input from the client has exactly the shape that our procedure expects. We will also return a simple text string from the query.
At the end of the file, we export the type of the router so we can use it in our frontend code in just a few moments.
constt =const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>const initTRPC: TRPCBuilder<object, object>initTRPC.create(); constTRPCBuilder<object, object>.create<RuntimeConfigOptions<object, object>>(opts?: RuntimeConfigOptions<object, object> | undefined): TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>router =const router: RouterBuilder<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>t.const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>router; constTRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure =t.const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure; constappRouter =const appRouter: BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>>router({const router: RouterBuilder <{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>(_: { greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }) => BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>>greeting:greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure .input(ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodObject<{ name: z.ZodString; }, z.core.$strip>>(schema: z.ZodObject<{ name: z.ZodString; }, z.core.$strip>): ProcedureBuilder<object, object, object, { name: string; }, { name: string; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>import zz.object({function object<{ name: z.ZodString; }>(shape?: { name: z.ZodString; } | undefined, params?: string | { error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined; message?: string | undefined | undefined; } | undefined): z.ZodObject<{ name: z.ZodString; }, z.core.$strip>name: z.ZodStringname:import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string() })) .query((ProcedureBuilder<object, object, object, { name: string; }, { name: string; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<`Hello ${string}`>(resolver: ProcedureResolver<object, object, object, { name: string; }, UnsetMarker, `Hello ${string}`>): QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>opts) => { const { input } =opts: ProcedureResolverOptions<object, object, object, { name: string; }>opts; return `Hello ${opts: ProcedureResolverOptions<object, object, object, { name: string; }>input.const input: { name: string; }name: stringname}` astype const = `Hello ${string}`const; }), }); export typeAppRouter = typeoftype AppRouter = Router<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>> & DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>appRouter;const appRouter: BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>>Create your HTTP server
AdaptersNext, we create our HTTP server using our
appRouter. We now have a tRPC server running!tRPC has many adapters so it can meet you where you are. Next.js, Express, the Fetch API (Astro, Remix, SvelteKit, Cloudflare Workers, etc.), Fastify, AWS Lambda, or a vanilla Node HTTP server.
const {listen } =const listen: { (port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): Server<typeof IncomingMessage, typeof ServerResponse>; (port?: number, hostname?: string, listeningListener?: () => void): Server<typeof IncomingMessage, typeof ServerResponse>; (port?: number, backlog?: number, listeningListener?: () => void): Server<typeof IncomingMessage, typeof ServerResponse>; (port?: number, listeningListener?: () => void): Server<...>; (path: string, backlog?: number, listeningListener?: () => void): Server<...>; (path: string, listeningListener?: () => void): Server<...>; (options: ListenOptions, listeningListener?: () => void): Server<...>; (handle: any, backlog?: number, listeningListener?: () => void): Server<...>; (handle: any, listeningListener?: () => void): Server<...>; }createHTTPServer({createHTTPServer<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>>(opts: CreateHTTPHandlerOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>>): Server<typeof IncomingMessage, typeof ServerResponse>router:BaseHandlerOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>, IncomingMessage>.router: BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>appRouter, }); // The API will now be listening on port 3000!const appRouter: BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>const listen: (port?: number, hostname?: string, backlog?: number, listeningListener?: () => void) => Server<typeof IncomingMessage, typeof ServerResponse> (+8 overloads)listen(3000);Connect your client and start querying!
The vanilla clientNow that we have the server running, we can create a client and start querying data.
We pass the
AppRoutertype when creating the client to give us TypeScript autocompletion and Intellisense that matches the backend API without requiring any code generation!consttrpc =const trpc: TRPCClient<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>>>createTRPCClient<createTRPCClient<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>>>(opts: CreateTRPCClientOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>>>): TRPCClient<...>AppRouter>({type AppRouter = Router<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>> & DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>links: [links: TRPCLink<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>>>[]httpBatchLink({httpBatchLink<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>>>(opts: HTTPBatchLinkOptions<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>): TRPCLink<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<...>>>url: string | URLurl: 'http://localhost:3000', }), ], }); const res = awaittrpc.const trpc: TRPCClient<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{ greeting: QueryProcedure<{ input: { name: string; }; output: `Hello ${string}`; meta: object; errorShape: DefaultErrorShape; }>; }>>>greeting.greeting: { query: Resolver<{ input: { name: string; }; output: `Hello ${string}`; errorShape: DefaultErrorShape; transformer: false; }>; }query({query: (input: { name: string; }, opts?: TRPCProcedureOptions) => TRPCTaggedPromise<`Hello ${string}`, DefaultErrorShape>name: stringname: 'John' });
Features
- Automatic typesafetyMade a server side change? TypeScript will warn you of errors on your client before you even save the file!
- Snappy DXtRPC has no build or compile steps, meaning no code generation, runtime bloat or build step.
- Framework agnosticCompatible with all JavaScript frameworks and runtimes. It's easy to add to your existing projects.
- AutocompletionUsing tRPC is like using an SDK for your API's server code, giving you confidence in your endpoints.
- Light bundle sizetRPC has zero dependencies and a tiny client-side footprint making it lightweight.
- Batteries includedWe provide adapters for React, Next.js, Express, Fastify, AWS Lambda, Solid, Svelte, and more.
Sponsors
tRPC is MIT licensed and built in the open, kept going by the companies and people below. Thank you!
