Define Procedures
Procedures in tRPC are very flexible primitives to create backend functions; they use a builder pattern which means you can create reusable base procedures for different parts of your backend application.
tip
- A procedure can be viewed as the equivalent of a REST-endpoint or a function.
- There's no internal difference between queries and mutations apart from semantics.
- Defining publicProcedure is the same for queries, mutations, and subscription with the exception that subscriptions need to return an
observable
instance.
Example without input validation
ts
import {router ,publicProcedure } from './trpc';import {z } from 'zod';constappRouter =router ({// Create publicProcedure at path 'hello'hello :publicProcedure .query (() => {return {greeting : 'hello world',};}),});
ts
import {router ,publicProcedure } from './trpc';import {z } from 'zod';constappRouter =router ({// Create publicProcedure at path 'hello'hello :publicProcedure .query (() => {return {greeting : 'hello world',};}),});
Input validation
tRPC works out-of-the-box with yup/superstruct/zod/myzod/custom validators/[..] - see test suite
With Zod
ts
import {publicProcedure ,router } from './trpc';import {z } from 'zod';export constappRouter =router ({hello :publicProcedure .input (z .object ({text :z .string (),}).optional (),).query (({input }) => {return {greeting : `hello ${input ?.text ?? 'world'}`,};}),});export typeAppRouter = typeofappRouter ;
ts
import {publicProcedure ,router } from './trpc';import {z } from 'zod';export constappRouter =router ({hello :publicProcedure .input (z .object ({text :z .string (),}).optional (),).query (({input }) => {return {greeting : `hello ${input ?.text ?? 'world'}`,};}),});export typeAppRouter = typeofappRouter ;
With Yup
tsx
import { initTRPC } from '@trpc/server';import * as yup from 'yup';export const t = initTRPC.create();export const appRouter = router({hello: publicProcedure.input(yup.object({text: yup.string().required(),}),).query(({ input }) => {return {greeting: `hello ${input?.text ?? 'world'}`,};}),});export type AppRouter = typeof appRouter;
tsx
import { initTRPC } from '@trpc/server';import * as yup from 'yup';export const t = initTRPC.create();export const appRouter = router({hello: publicProcedure.input(yup.object({text: yup.string().required(),}),).query(({ input }) => {return {greeting: `hello ${input?.text ?? 'world'}`,};}),});export type AppRouter = typeof appRouter;
With Superstruct
tsx
import { initTRPC } from '@trpc/server';import { defaulted, object, string } from 'superstruct';export const t = initTRPC.create();export const appRouter = router({hello: publicProcedure.input(object({/*** Also supports inline doc strings when referencing the type.*/text: defaulted(string(), 'world'),}),).query(({ input }) => {return {greeting: `hello ${input.text}`,};}),});export type AppRouter = typeof appRouter;
tsx
import { initTRPC } from '@trpc/server';import { defaulted, object, string } from 'superstruct';export const t = initTRPC.create();export const appRouter = router({hello: publicProcedure.input(object({/*** Also supports inline doc strings when referencing the type.*/text: defaulted(string(), 'world'),}),).query(({ input }) => {return {greeting: `hello ${input.text}`,};}),});export type AppRouter = typeof appRouter;
Multiple input parsers
You're able to chain multiple parsers in order to make reusable publicProcedures for different parts of your application.
ts
// ------------------------------// @filename: roomProcedure.ts// ------------------------------import {publicProcedure } from './trpc';import {z } from 'zod';/*** Create reusable publicProcedure for a chat room*/export constroomProcedure =publicProcedure .input (z .object ({roomId :z .string (),}),);// ------------------------------// @filename: _app.ts// ------------------------------import {router } from './trpc';import {roomProcedure } from './roomProcedure';import {z } from 'zod';constappRouter =router ({sendMessage :roomProcedure // Add extra input validation for the `sendMessage`-procedure.input (z .object ({text :z .string (),}),).mutation (({input }) => {// [...]}),});export typeAppRouter = typeofappRouter ;
ts
// ------------------------------// @filename: roomProcedure.ts// ------------------------------import {publicProcedure } from './trpc';import {z } from 'zod';/*** Create reusable publicProcedure for a chat room*/export constroomProcedure =publicProcedure .input (z .object ({roomId :z .string (),}),);// ------------------------------// @filename: _app.ts// ------------------------------import {router } from './trpc';import {roomProcedure } from './roomProcedure';import {z } from 'zod';constappRouter =router ({sendMessage :roomProcedure // Add extra input validation for the `sendMessage`-procedure.input (z .object ({text :z .string (),}),).mutation (({input }) => {// [...]}),});export typeAppRouter = typeofappRouter ;
Multiple Procedures
To add multiple publicProcedures, you define them as properties on the object passed to t.router()
.
tsx
import { initTRPC } from '@trpc/server';export const t = initTRPC.create();const router = t.router;const publicProcedure = t.procedure;export const appRouter = router({hello: publicProcedure.query(() => {return {text: 'hello world',};}),bye: publicProcedure.query(() => {return {text: 'goodbye',};}),});export type AppRouter = typeof appRouter;
tsx
import { initTRPC } from '@trpc/server';export const t = initTRPC.create();const router = t.router;const publicProcedure = t.procedure;export const appRouter = router({hello: publicProcedure.query(() => {return {text: 'hello world',};}),bye: publicProcedure.query(() => {return {text: 'goodbye',};}),});export type AppRouter = typeof appRouter;
Reusable base procedures
You can create reusable base procedures to have a set of login-protected procedures.
tip
This can be combined with multiple input parsers & metadata to create powerful reusable authorization and authentication patterns.
tsx
// -------------------------------------------------// @filename: context.ts// -------------------------------------------------import {inferAsyncReturnType } from '@trpc/server';import * astrpcNext from '@trpc/server/adapters/next';import {getSession } from 'next-auth/react';/*** Creates context for an incoming request* @link https://trpc.io/docs/context*/export async functioncreateContext (opts :trpcNext .CreateNextContextOptions ) {constsession = awaitgetSession ({req :opts .req });return {session ,};};export typeContext =inferAsyncReturnType <typeofcreateContext >;// -------------------------------------------------// @filename: trpc.ts// -------------------------------------------------import {initTRPC ,TRPCError } from '@trpc/server';import {Context } from './context';constt =initTRPC .context <Context >().create ();/*** Reusable middleware that checks if users are authenticated.**/constisAuthed =t .middleware (({next ,ctx }) => {if (!ctx .session ?.user ?.throw newTRPCError ({code : 'UNAUTHORIZED',});}returnnext ({ctx : {// Infers the `session` as non-nullablesession :ctx .session ,},});});export constmiddleware =t .middleware ;export constrouter =t .router ;/*** Unprotected procedure**/export constpublicProcedure =t .procedure ;/*** Protected procedure**/export constprotectedProcedure =t .procedure .use (isAuthed );// -------------------------------------------------// @filename: _app.ts// -------------------------------------------------import {protectedProcedure ,publicProcedure ,router } from './trpc';import {z } from 'zod';export constappRouter =router ({createPost :protectedProcedure .mutation (({ctx }) => {constsession =ctx .session ;// [...]}),whoami :publicProcedure .query (({ctx }) => {constsession =ctx .session ;// [...]}),});
tsx
// -------------------------------------------------// @filename: context.ts// -------------------------------------------------import {inferAsyncReturnType } from '@trpc/server';import * astrpcNext from '@trpc/server/adapters/next';import {getSession } from 'next-auth/react';/*** Creates context for an incoming request* @link https://trpc.io/docs/context*/export async functioncreateContext (opts :trpcNext .CreateNextContextOptions ) {constsession = awaitgetSession ({req :opts .req });return {session ,};};export typeContext =inferAsyncReturnType <typeofcreateContext >;// -------------------------------------------------// @filename: trpc.ts// -------------------------------------------------import {initTRPC ,TRPCError } from '@trpc/server';import {Context } from './context';constt =initTRPC .context <Context >().create ();/*** Reusable middleware that checks if users are authenticated.**/constisAuthed =t .middleware (({next ,ctx }) => {if (!ctx .session ?.user ?.throw newTRPCError ({code : 'UNAUTHORIZED',});}returnnext ({ctx : {// Infers the `session` as non-nullablesession :ctx .session ,},});});export constmiddleware =t .middleware ;export constrouter =t .router ;/*** Unprotected procedure**/export constpublicProcedure =t .procedure ;/*** Protected procedure**/export constprotectedProcedure =t .procedure .use (isAuthed );// -------------------------------------------------// @filename: _app.ts// -------------------------------------------------import {protectedProcedure ,publicProcedure ,router } from './trpc';import {z } from 'zod';export constappRouter =router ({createPost :protectedProcedure .mutation (({ctx }) => {constsession =ctx .session ;// [...]}),whoami :publicProcedure .query (({ctx }) => {constsession =ctx .session ;// [...]}),});