Input Validation
Validating procedure inputs with Zod schemas.
All RPC procedure inputs are validated with Zod schemas. oRPC integrates Zod seamlessly -- the input types are inferred from your schema and flow through to the client with full type safety.
The .input() Pattern
Attach a Zod schema to any procedure using .input():
import { z } from "zod"
export const myProcedure = protectedProcedure
.input(z.object({
id: z.string().min(1),
title: z.string().min(1).max(200),
status: z.enum(["active", "archived"]).optional(),
}))
.handler(async ({ context, input }) => {
// input is fully typed:
// { id: string; title: string; status?: "active" | "archived" | undefined }
})The input parameter in the handler is automatically typed based on the Zod schema. You get full autocomplete and type checking without any manual type annotations.
Error Handling
When input validation fails, oRPC returns a structured error to the client. For application-level errors, throw ORPCError:
import { ORPCError } from "@orpc/server"
export const documentRouter = {
get: protectedProcedure
.input(z.object({ id: z.string().min(1) }))
.handler(async ({ context, input }) => {
const db = createDb()
const [doc] = await db
.select()
.from(documents)
.where(and(eq(documents.id, input.id), eq(documents.userId, context.session.user.id)))
.limit(1)
if (!doc) {
throw new Error("Document not found")
}
return doc
}),
}Use Error for domain errors (like "not found") and ORPCError for framework-level errors (like UNAUTHORIZED). Both are caught by the global onError interceptor and logged.
Type Inference
The types flow automatically from server to client. No code generation needed:
// Server: packages/rpc/src/routers/profile.ts
export const profileRouter = {
update: protectedProcedure
.input(z.object({
defaultModel: z.string().optional(),
preferredLocale: z.string().optional(),
outputTone: z.string().optional(),
marketingOptIn: z.boolean().optional(),
}))
.handler(async ({ context, input }) => {
// input type: { defaultModel?: string; preferredLocale?: string; ... }
}),
}
// Client: the input type is inferred automatically
orpc.profile.update.useMutation({
mutationFn: (input) => orpc.profile.update.mutate(input),
// input is typed: { defaultModel?: string; preferredLocale?: string; ... }
})Real-World Examples
Profile Update
update: protectedProcedure
.input(z.object({
defaultModel: z.string().optional(),
preferredLocale: z.string().optional(),
outputTone: z.string().optional(),
marketingOptIn: z.boolean().optional(),
}))
.handler(async ({ context, input }) => {
const db = createDb()
const userId = context.session.user.id
const [existing] = await db
.select()
.from(profiles)
.where(eq(profiles.userId, userId))
.limit(1)
if (existing) {
const [updated] = await db
.update(profiles)
.set({ ...input, updatedAt: new Date() })
.where(eq(profiles.userId, userId))
.returning()
return updated
}
const [created] = await db
.insert(profiles)
.values({ userId, ...input })
.returning()
return created
})Document Get
get: protectedProcedure
.input(z.object({ id: z.string().min(1) }))
.handler(async ({ context, input }) => {
const db = createDb()
const [doc] = await db
.select()
.from(documents)
.where(and(eq(documents.id, input.id), eq(documents.userId, context.session.user.id)))
.limit(1)
if (!doc) throw new Error("Document not found")
return doc
})Conversation Create
create: protectedProcedure
.input(z.object({
title: z.string().min(1),
task: z.string().optional(),
}))
.handler(async ({ context, input }) => {
const db = createDb()
const [convo] = await db
.insert(conversations)
.values({
userId: context.session.user.id,
title: input.title,
task: input.task ?? "summarize",
})
.returning()
return convo
})Conversation Send Message
sendMessage: protectedProcedure
.input(z.object({
conversationId: z.string().min(1),
content: z.string().min(1),
}))
.handler(async ({ context, input }) => {
// Validates that both conversationId and content are non-empty strings
// Returns typed response with userMessage and assistantMessage
})Validation Patterns
z.string().min(1)Rejects empty strings.
z.string().optional()Allows string | undefined.
z.enum(["active", "archived"])Restricts to specific string values.
z.boolean().optional()Allows boolean | undefined.
z.object({ id: z.string(), name: z.string() })Nested object validation.