From harness-claude
Defines type-safe inputs and outputs for tRPC procedures using Zod schemas, enabling end-to-end TypeScript inference, runtime validation, and client-server schema sharing.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Define type-safe inputs and outputs with Zod schemas for end-to-end type inference
Guides tRPC end-to-end typesafe API development with router architecture, procedure design, Zod input validation, middleware chaining via unstable_pipe, TRPCError handling, and Vertical Slice pattern.
Builds end-to-end type-safe tRPC APIs with routers, procedures, middleware, subscriptions, and Next.js/React integration for TypeScript full-stack apps.
Organizes tRPC procedures into nested routers for multi-domain APIs like users and posts, merging them into a single appRouter while preserving end-to-end type safety. Use for splitting large APIs across files.
Share bugs, ideas, or general feedback.
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