From harness-claude
Adds Zod input/output validation to tRPC procedures, enabling automatic type inference and runtime validation. Share schemas between client and server for type safety.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:trpc-input-validationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Define type-safe inputs and outputs with Zod schemas for end-to-end type inference
Define type-safe inputs and outputs with Zod schemas for end-to-end type inference
superjson to pass complex types (Dates, Maps) through tRPC procedures.input(zodSchema) on any procedure to define validated input — tRPC rejects requests that do not match the schema.z.infer<typeof schema> — share this type between client and server..output(zodSchema) to define the expected output shape — tRPC validates and strips unknown fields at runtime.z.object() for structured inputs, z.string().uuid() for IDs, and z.enum() for fixed option sets..optional(), .default(), and .nullish() on Zod fields for optional procedure inputs.superjson transformer for procedures that pass Date, BigInt, or other non-JSON-serializable types.schemas/ directory — import them in both the router and the client form validation.// schemas/post.ts — shared Zod schemas
import { z } from 'zod';
export const createPostSchema = z.object({
title: z.string().min(1, 'Title required').max(200),
content: z.string().min(1),
published: z.boolean().default(false),
tags: z.array(z.string()).max(10).default([]),
});
export const updatePostSchema = createPostSchema.partial().extend({
id: z.string().cuid(),
});
export const postFiltersSchema = z.object({
status: z.enum(['draft', 'published', 'archived']).optional(),
authorId: z.string().cuid().optional(),
limit: z.number().int().min(1).max(100).default(20),
cursor: z.string().optional(),
});
export type CreatePostInput = z.infer<typeof createPostSchema>;
// server/routers/posts.ts — using schemas in procedures
import { router, publicProcedure, protectedProcedure } from '../trpc';
import { createPostSchema, updatePostSchema, postFiltersSchema } from '@/schemas/post';
export const postsRouter = router({
list: publicProcedure.input(postFiltersSchema).query(({ ctx, input }) =>
ctx.db.post.findMany({
where: { status: input.status, authorId: input.authorId },
take: input.limit,
cursor: input.cursor ? { id: input.cursor } : undefined,
})
),
create: protectedProcedure
.input(createPostSchema)
.mutation(({ ctx, input }) =>
ctx.db.post.create({ data: { ...input, authorId: ctx.session.user.id } })
),
});
tRPC's type inference flows from Zod schemas through procedure definitions to the client. When you call api.posts.create.useMutation(), the variables type is automatically inferred from the .input() schema — no manual type annotation required.
.input() vs .output() usage: .input() is nearly universal — every procedure that accepts parameters should use it. .output() is more situational — use it when you want to guarantee the return shape (strip extra fields from DB objects) or when documenting a public API contract. Output validation adds runtime overhead for every procedure call.
Zod schema reuse between client and server: Define schemas in a shared location (e.g., src/schemas/ or a @repo/schemas monorepo package). Import them in the tRPC router for server-side validation AND in React Hook Form or Zod's safeParse for client-side form validation. One schema, two uses, zero drift.
Partial schemas for updates: createPostSchema.partial() makes all fields optional — perfect for PATCH-style update procedures where only the changed fields are sent. Add back required fields (like id) with .extend({ id: z.string() }).
Procedure chaining: Procedures are built by chaining: t.procedure.use(middleware).input(schema).query(handler). The order matters — .use() must come before .input(). The ctx type in the handler reflects all middleware transformations applied before it.
superjson for Dates: Without superjson, JSON serialization converts Date to string. With superjson, Date round-trips correctly. Add it to initTRPC.create({ transformer: superjson }) and the corresponding client link. All procedures in the router automatically use it.
https://trpc.io/docs/server/validators
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeBuilds end-to-end type-safe tRPC APIs with routers, procedures, middleware, subscriptions, and Next.js/React integration for TypeScript full-stack apps.
Builds tRPC APIs with Zod validation, middleware chaining, Vertical Slice architecture, and domain error handling.
Throws typed TRPCErrors in procedures and formats them consistently for client consumption. Covers error codes, Zod validation details, error formatters, and client-side handling.