
Description
Configure input and output validation with .input() and .output() using Zod, Yup, Superstruct, ArkType, Valibot, Effect, or custom validator functions. Chain multiple .input() calls to merge object schemas. Standard Schema protocol support. Output validation returns INTERNAL_SERVER_ERROR on failure.
SKILL.md
tRPC -- Validators
Setup
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
// server/appRouter.ts
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
hello: publicProcedure
.input(z.object({ name: z.string() }))
.output(z.object({ greeting: z.string() }))
.query(({ input }) => {
return { greeting: `hello ${input.name}` };
}),
});
export type AppRouter = typeof appRouter;
Core Patterns
Input validation with Zod
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
userById: publicProcedure.input(z.string()).query(({ input }) => {
return { id: input, name: 'Katt' };
}),
userCreate: publicProcedure
.input(z.object({ name: z.string(), email: z.string().email() }))
.mutation(({ input }) => {
return { id: '1', ...input };
}),
});
Input chaining to merge object schemas
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
const baseProcedure = t.procedure
.input(z.object({ townName: z.string() }))
.use((opts) => {
console.log(`Request from: ${opts.input.townName}`);
return opts.next();
});
export const appRouter = t.router({
hello: baseProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return { greeting: `Hello ${input.name}, from ${input.townName}` };
}),
});
Multiple .input() calls merge object properties; the final input type is { townName: string; name: string }.
Output validation
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
export const appRouter = router({
hello: publicProcedure
.output(z.object({ greeting: z.string() }))
.query(() => {
return { greeting: 'hello world' };
}),
});
Output validation catches mismatches between your return type and the expected shape, useful for untrusted data sources.
Custom validator function (no library)
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
hello: t.procedure
.input((value): string => {
if (typeof value === 'string') return value;
throw new Error('Input is not a string');
})
.output((value): string => {
if (typeof value === 'string') return value;
throw new Error('Output is not a string');
})
.query(({ input }) => {
return `hello ${input}`;
}),
});
Common Mistakes
MEDIUM Chaining non-object inputs
Wrong:
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure.input(z.string()).input(z.number());
Correct:
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure
.input(z.object({ name: z.string() }))
.input(z.object({ age: z.number() }));
Multiple .input() calls merge object properties; non-object schemas (string, number, array) cannot be merged and produce type errors.
Source: www/docs/server/validators.md
MEDIUM Output validation failure returns 500
Wrong:
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure
.output(z.object({ id: z.string() }))
.query(() => ({ id: 123 }));
Correct:
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure
.output(z.object({ id: z.string() }))
.query(() => ({ id: '123' }));
If .output() validation fails, tRPC returns INTERNAL_SERVER_ERROR (500), not BAD_REQUEST, because the server produced invalid data.
Source: www/docs/server/validators.md
HIGH Using cursor: z.optional() without nullable for infinite queries
Wrong:
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure
.input(z.object({ cursor: z.string().optional() }))
.query(({ input }) => {
return { items: [], nextCursor: input.cursor };
});
Correct:
import { z } from 'zod';
import { publicProcedure } from './trpc';
const proc = publicProcedure
.input(z.object({ cursor: z.string().nullish() }))
.query(({ input }) => {
return { items: [], nextCursor: input.cursor };
});
React Query internally passes cursor: undefined during invalidation refetch; using .optional() without .nullable() can fail validation. Use .nullish() instead.
Source: https://github.com/trpc/trpc/issues/6862
See Also
server-setup-- initTRPC, routers, procedureserror-handling-- how validation errors surface as BAD_REQUESTerror-handling-- errorFormatter to expose Zod field errorsmiddlewares-- use input chaining with middleware base procedures
More skills from the trpc repository
View all 24 skillsadapter-aws-lambda
deploy tRPC APIs on AWS Lambda
Jul 18API DevelopmentAWSBackendtRPCadapter-express
mount tRPC as Express middleware
Jul 18API DevelopmentBackendExpresstRPCadapter-fastify
mount tRPC as a Fastify plugin
Jul 18API DevelopmentBackendFastifytRPCadapter-fetch
deploy tRPC on edge runtimes
Jul 18CloudflareDeploymentEdgetRPC +1adapter-standalone
mount tRPC on Node.js servers
Jul 18API DevelopmentBackendNode.jstRPCauth
implement authentication in tRPC applications
Jul 18AuthAuthenticationBackendtRPC
More from tRPC
View publishercaching
configure HTTP caching for tRPC queries
trpc
Jul 18BackendCachingPerformancetRPCclient-setup
set up vanilla tRPC clients
trpc
Jul 18API DevelopmentFrontendtRPCerror-handling
implement typed error handling in tRPC
trpc
Jul 18BackendDebuggingtRPCTypeScriptlinks
configure tRPC client link chains
trpc
Jul 18API DevelopmentFrontendtRPCmiddlewares
compose middleware for tRPC procedures
trpc
Jul 18BackendMiddlewaretRPCnextjs-app-router
implement tRPC in Next.js App Router
trpc
Jul 18BackendFrontendNext.jstRPC +1