Mutation procedure definition with input validation
Define a mutation procedure using t.procedure.input(zodSchema).mutation(callback). The input is validated using Zod schema. The mutation callback receives opts parameter containing opts.input with the validated input data.
tRPC permission and shield system
tRPC Shield is a permission system for tRPC that can be automatically generated from Prisma schemas using the Prisma tRPC Shield Generator.
Prisma integration tools for tRPC
Community projects provide Prisma integration with tRPC: Prisma tRPC Generator automatically generates tRPC routers from Prisma schemas, Prisma tRPC Shield Generator generates tRPC Shield permissions from Prisma schema, and ZenStack is a full-stack toolkit that adds access control to Prisma and generates tRPC routers from schema.
Applying middleware to full routers
Middleware cannot be applied at the full router level in tRPC. Instead, use base procedures which offer more flexibility.
Dynamic output depending on input not supported
tRPC does not currently support dynamically returning different output types based on input because TypeScript does not yet support Higher Kinded Types.
Basic tRPC router and procedure setup
Import initTRPC from @trpc/server and call initTRPC.create() to get access to router and procedure builders. Extract t.router and t.procedure to create router definitions. A basic router is created by calling router() with an object mapping procedure names to procedure definitions.
Complete Step 1 example: greeting query with Zod validation
import { initTRPC } from '@trpc/server';
import z from 'zod';
const t = initTRPC.create();
const router = t.router;
const publicProcedure = t.procedure;
const appRouter = router({
greeting: publicProcedure
.input(z.object({ name: z.string() }))
.query((opts) => {
const { input } = opts;
return `Hello ${input.name}` as const;
}),
});
export type AppRouter = typeof appRouter;
Basic server setup with initTRPC and t.router
To set up a basic tRPC server, import initTRPC from '@trpc/server', call initTRPC.create() to get a context object t, then use t.router() to define your application router with procedures. Each procedure is defined using t.procedure and can be configured with input validation, output validation, and handlers.
Define a query procedure with input validation using Zod
A query procedure is defined by chaining .input() with a Zod schema to validate incoming data, then calling .query() with a handler function. The handler receives an options object containing the validated input. Example: t.procedure.input(z.object({ name: z.string() })).query((opts) => { const { input } = opts; return `Hello ${input.name}`; })
Nested procedure path format
Nested procedures are separated by dots in the URL path. For example, a nested procedure defined as router({ post: router({ byId: ... }) }) is accessed at /api/trpc/post.byId
Query procedure with publicProcedure.query()
Use publicProcedure.query() to define a query procedure. Query procedures use HTTP GET and are intended for read operations that do not cause side effects.
tRPC server initialization with initTRPC
Create a tRPC backend instance using initTRPC.create(). This should be done only once per backend. Export reusable router and procedure helpers from a separate file to avoid cyclic dependencies and enable their use throughout the router.
Define app router with procedures
Create an appRouter using the router function and export its type as AppRouter. The AppRouter type must be exported for use on the client side to enable end-to-end type safety.
Mutation procedure with publicProcedure.mutation()
Use publicProcedure.mutation() to define a mutation procedure. Mutation procedures use HTTP POST and are intended for operations that cause side effects. Mutations are semantically similar to queries but indicate that the operation modifies data.
Mutation procedure example with Zod input
userCreate: publicProcedure
.input(z.object({ name: z.string() }))
.mutation(async (opts) => {
const { input } = opts;
const user: User = { id: '1', ...input };
return user;
});
Recommended file structure for tRPC backend
Separate tRPC code into three files to prevent cyclic dependencies: server/trpc.ts for tRPC instantiation, server/appRouter.ts for router definition and type export, and server/index.ts for HTTP server setup.
TRPCProcedureOptions moved to @trpc/client
TRPCProcedureOptions has been moved from @trpc/server to @trpc/client. Code using ProcedureOptions from @trpc/server should be updated to import TRPCProcedureOptions from @trpc/client instead.
TRPCRequestInfo inputs materialized lazily
In v11, inputs are materialized lazily when required by the procedure, so input and procedure type are no longer available when tRPC calls createContext. Access the input by calling info.calls[index].getRawInput().
Lazy-loading routers
tRPC v11 adds support for lazy-loading routers. Internally, the callProcedure() method now receives { router: AnyRouter } instead of { _def: AnyRouter['_def'] }.
Short-hand router definitions
Router definitions now support shorthand plain object syntax for creating sub-routers. Instead of router({ proc: publicProcedure.query(...) }), you can use { proc: publicProcedure.query(...) } directly as a shorthand.
rawInput in middleware changed to getRawInput
The middleware rawInput property has been replaced with getRawInput() function call. This change supports handling content types other than JSON in the future.
inferProcedureBuilderResolverOptions helper
A new inferProcedureBuilderResolverOptions<T> helper has been added to infer options for a procedure builder resolver, enabling creation of reusable functions for different procedures.
Create tRPC router basic example
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
export const t = initTRPC.create();
export const appRouter = t.router({
getUser: t.procedure.input(z.string()).query((opts) => {
opts.input; // string
return { id: opts.input, name: 'Bilbo' };
}),
createUser: t.procedure
.input(z.object({ name: z.string().min(5) }))
.mutation(async (opts) => {
// use your ORM of choice
return { id: '1', ...opts.input };
}),
});
// export type definition of API
export type AppRouter = typeof appRouter;
Router splitting with merging
If a router file becomes too large, split it into multiple subrouters implemented in separate files, then merge them into a single root appRouter.
Sample router with input validation using Zod
A sample tRPC router with input validation using Zod:
```ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
import type { Context } from './context';
type User = {
id: string;
name: string;
bio?: string;
};
const users: Record<string, User> = {};
export const t = initTRPC.context<Context>().create();
export const appRouter = t.router({
getUserById: t.procedure.input(z.string()).query((opts) => {
return users[opts.input];
}),
createUser: t.procedure
.input(
z.object({
name: z.string().min(3),
bio: z.string().max(142).optional(),
}),
)
.mutation((opts) => {
const id = Date.now().toString();
const user: User = { id, ...opts.input };
users[user.id] = user;
return user;
}),
});
export type AppRouter = typeof appRouter;
```
Example: flat router merging with mergeRouters
import { mergeRouters } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';
const appRouter = mergeRouters(userRouter, postRouter);
export type AppRouter = typeof appRouter;
Merging routers as nested namespaces
Routers can be merged by nesting them as properties in a parent router object. This creates a hierarchical namespace where procedures are accessed via parent.child.procedureName. For example, a router can have user and post properties, each containing their own routers with procedures like list and create.
t.mergeRouters flattens router procedures
The t.mergeRouters function flattens multiple routers into a single namespace. Instead of accessing procedures as parent.child.procedureName, all procedures become available at the top level. This is useful when you want a flat procedure namespace rather than nested namespaces.
lazy function for dynamic router loading
The lazy function from @trpc/server enables dynamic loading of routers to reduce cold starts. It accepts an import function and can be used in two ways: Option 1 uses the shorthand when a module exports exactly one router (lazy(() => import('./greeting.js'))), which automatically exports the default or single export. Option 2 explicitly specifies which router to load when a module exports multiple routers (lazy(() => import('./user.js').then((m) => m.userRouter))). Usage of lazy-loaded routers is identical to normal routers after loading.
Example: nested router merging
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';
const appRouter = router({
user: userRouter,
post: postRouter,
});
appRouter.user
appRouter.post
export type AppRouter = typeof appRouter;
Example: lazy-loading routers
import { lazy } from '@trpc/server';
import { router } from '../trpc';
export const appRouter = router({
// Option 1: Short-hand when the module has exactly 1 router exported
greeting: lazy(() => import('./greeting.js')),
// Option 2: if exporting more than 1 router
user: lazy(() => import('./user.js').then((m) => m.userRouter)),
});
export type AppRouter = typeof appRouter;
Three procedure types in tRPC
tRPC procedures can be one of three types: Query (used to fetch data, generally does not change any data), Mutation (used to send data, often for create/update/delete purposes), or Subscription (documented separately).
Procedures use immutable builder pattern
Procedures in tRPC are built using an immutable builder pattern, which allows you to create reusable base procedures that share functionality among multiple procedures.
t.procedure is the base procedure from initTRPC
The t object created during tRPC setup using initTRPC provides an initial t.procedure which all other procedures are built on. It is recommended to rename and export this as publicProcedure.
Defining a simple query procedure
A query procedure is defined using publicProcedure.query() and should be used as the best place to fetch data without changing any data.
Defining a simple mutation procedure
A mutation procedure is defined using publicProcedure.mutation() and should be used as the best place to do things like updating a database. The mutation receives opts parameter which includes ctx for accessing context.
Base procedures pattern for code reuse
Base procedures are a key pattern for code and behaviour reuse in tRPC. The pattern involves creating named procedures for specific use cases (beyond publicProcedure) such as authedProcedure for logged-in users or organizationProcedure for organization-specific operations. Every application is likely to need this pattern.
Chaining procedures to build specialized base procedures
Specialized base procedures can be built by chaining methods on existing procedures. For example, organizationProcedure extends authedProcedure by chaining .input() to validate organizationId and .use() to verify membership.
Define a router with procedures
import { publicProcedure, router } from './trpc';
const appRouter = router({
greeting: publicProcedure.query(() => 'hello tRPC v11!'),
});
export type AppRouter = typeof appRouter;
Inline sub-router definition
Sub-routers can be defined inline using plain JavaScript objects. An inline sub-router like { proc: publicProcedure.query(() => '...') } is equivalent to using router({ proc: publicProcedure.query(() => '...') }). Both approaches are equal in functionality.
Nested router example with inline sub-routers
const appRouter = router({
nested1: router({
proc: publicProcedure.query(() => '...'),
}),
nested2: {
proc: publicProcedure.query(() => '...'),
},
});
Method chaining for tRPC initialization
import { initTRPC } from '@trpc/server';
type Context = { userId: string };
type Meta = { description: string };
const t = initTRPC.context<Context>().meta<Meta>().create({
/* [...] */
});
RootConfig runtime configuration interface
The RootConfig interface defines runtime configuration options for tRPC: transformer (DataTransformerOptions) for data transformation, errorFormatter (ErrorFormatter) for custom error formatting, allowOutsideOfServer (boolean, default false) to allow @trpc/server outside server environments with a warning about testing use only, isServer (boolean, default typeof window === 'undefined' || 'Deno' in window || process.env.NODE_ENV === 'test') to indicate server environment, and isDev (boolean, default process.env.NODE_ENV !== 'production') to determine if stack traces should be returned.
Initialize tRPC with context and metadata
When initializing tRPC, you can set up request contexts and assign metadata to procedures. These are configured by chaining methods on the t-object before calling .create().
Initialize tRPC exactly once per application
tRPC should be initialized exactly once per application. Multiple instances of tRPC will cause issues. Use initTRPC.create() to create a single instance that should be stored and reused throughout the application.
createCallerFactory creates server-side router caller
The t.createCallerFactory() function creates a server-side caller for any router. You first call createCallerFactory with the router as an argument, then it returns a function where you pass in a Context for the following procedure calls. This is useful for server-side calls and integration testing of tRPC procedures.
Do not call createCaller from within procedures
Do not use createCaller to call procedures from within other procedures. This creates overhead by potentially creating context again, executing all middlewares, and validating the input - all of which were already done by the current procedure. Instead, extract the shared logic into a separate function and call that from within the procedures.
createCallerFactory basic usage pattern
To use createCallerFactory: first call createCallerFactory(appRouter) to create a caller factory function; then call the returned function with a Context object to create a caller instance; finally call procedures on the caller instance like caller.post.add({...}) or caller.post.list().
router.createCaller() method signature
The router.createCaller() method takes a Context as its first argument and returns a RouterCaller instance. It can also take an optional second argument with an onError option for error handling.
createCaller example with greeting query
Example showing createCaller with a query procedure:
```ts
const t = initTRPC.create();
const router = t.router({
greeting: t.procedure
.input(z.object({ name: z.string() }))
.query((opts) => `Hello ${opts.input.name}`),
});
const caller = router.createCaller({});
const result = await caller.greeting({ name: 'tRPC' });
```
createCaller example with mutation
Example showing createCaller with a mutation procedure:
```ts
const posts = ['One', 'Two', 'Three'];
const t = initTRPC.create();
const router = t.router({
post: t.router({
add: t.procedure.input(z.string()).mutation((opts) => {
posts.push(opts.input);
return posts;
}),
}),
});
const caller = router.createCaller({});
const result = await caller.post.add('Four');
```
Integration test with createCaller example
Example showing createCaller usage in an integration test:
```ts
async function testAddAndGetPost() {
const ctx = await createContextInner({});
const caller = createCaller(ctx);
const input: inferProcedureInput<AppRouter['post']['add']> = {
text: 'hello test',
title: 'hello test',
};
const post = await caller.post.add(input);
const byId = await caller.post.byId({ id: post.id });
}
```
createCallerFactory with appRouter export pattern
When exporting createCaller from a router file, the pattern is: export const createCaller = t.createCallerFactory(appRouter);. This allows the created caller to be imported and used in other files, such as test files.
Router nesting with t.router
Nest routers by passing an object to t.router() where each key is a sub-router name. For example, t.router({ post: t.router({ all: ..., byId: ... }) }) creates a 'post' namespace containing multiple procedures.
Query procedure with input validation
Create a query procedure by calling t.procedure.input() with a Zod schema, then .query() with a handler function. The handler receives an object with input property containing validated parameters. Example: t.procedure.input(z.object({ id: z.string() })).query(({ input }) => { ... })
Initialize tRPC with initTRPC.create
Initialize a tRPC instance by importing initTRPC from '@trpc/server', calling initTRPC.create(), and storing the result in a variable like const t. This instance is used to define procedures and routers.
Basic tRPC server initialization
Import initTRPC from @trpc/server and call initTRPC.create() to get a reference t. Export router and procedure from t: const t = initTRPC.create(); export const router = t.router; export const procedure = t.procedure;
Define a basic tRPC router with procedure
Use the router and procedure helpers to define routes. Example: router({ hello: procedure.input(z.object({ text: z.string() })).query(({ input }) => ({ greeting: `hello ${input.text}` })) }). Export the router type as AppRouter.
tRPC routers location in Next.js starter
In the next-prisma-websockets-starter example, the application's different tRPC routers are located in ./src/server/routers