install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/zod-nextjs-integration" ~/.claude/skills/intense-visions-harness-engineering-zod-nextjs-integration-df39b8 && rm -rf "$T"
manifest:
agents/skills/codex/zod-nextjs-integration/SKILL.mdsource content
Zod Next.js Integration
Validate Next.js server actions, API routes, and form data with Zod schemas
When to Use
- Validating inputs to Next.js server actions (App Router)
- Parsing and validating request bodies in API route handlers
- Handling form data validation with
/useFormStateuseActionState - Validating search params and dynamic route segments
Instructions
- Validate server action inputs — the action receives
or typed arguments:FormData
'use server'; import { z } from 'zod'; import { redirect } from 'next/navigation'; const CreatePostSchema = z.object({ title: z.string().min(1, 'Title is required').max(100), content: z.string().min(10, 'Content is too short'), published: z.coerce.boolean().default(false), }); export async function createPost(formData: FormData) { const result = CreatePostSchema.safeParse({ title: formData.get('title'), content: formData.get('content'), published: formData.get('published'), }); if (!result.success) { return { success: false, errors: result.error.flatten().fieldErrors }; } const post = await db.post.create({ data: result.data }); redirect(`/posts/${post.id}`); }
- Use
(React 19) oruseActionState
with typed state:useFormState
'use client' import { useActionState } from 'react' import { createPost } from './actions' type FormState = { success: boolean errors?: { title?: string[]; content?: string[] } } const initialState: FormState = { success: false } export function CreatePostForm() { const [state, formAction, isPending] = useActionState(createPost, initialState) return ( <form action={formAction}> <input name="title" /> {state.errors?.title && <p>{state.errors.title[0]}</p>} <textarea name="content" /> {state.errors?.content && <p>{state.errors.content[0]}</p>} <button disabled={isPending}>Create</button> </form> ) }
- Validate API route handler inputs in the App Router:
import { z } from 'zod'; import { NextRequest, NextResponse } from 'next/server'; const CreateUserSchema = z.object({ name: z.string().min(1), email: z.string().email(), role: z.enum(['admin', 'viewer']).default('viewer'), }); export async function POST(request: NextRequest) { const body = await request.json().catch(() => null); if (!body) { return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); } const result = CreateUserSchema.safeParse(body); if (!result.success) { return NextResponse.json( { success: false, errors: result.error.flatten().fieldErrors }, { status: 422 } ); } const user = await db.user.create({ data: result.data }); return NextResponse.json({ success: true, user }, { status: 201 }); }
- Validate search params:
import { z } from 'zod'; const SearchParamsSchema = z.object({ page: z.coerce.number().int().positive().default(1), limit: z.coerce.number().int().min(1).max(100).default(20), query: z.string().optional(), sort: z.enum(['asc', 'desc']).default('desc'), }); // In a Server Component: export default function Page({ searchParams }: { searchParams: Record<string, string> }) { const params = SearchParamsSchema.parse(searchParams); // params.page, params.limit, params.query, params.sort are all properly typed }
- Validate dynamic route params:
const RouteParamsSchema = z.object({ id: z.string().uuid('Invalid post ID format'), }); export default async function PostPage({ params }: { params: { id: string } }) { const result = RouteParamsSchema.safeParse(params); if (!result.success) { notFound(); } const post = await db.post.findUniqueOrThrow({ where: { id: result.data.id } }); // ... }
- Create a reusable validation wrapper for server actions:
import { z } from 'zod'; type ActionResult<T> = | { success: true; data: T } | { success: false; errors: Record<string, string[]> }; export function createValidatedAction<TSchema extends z.ZodSchema, TResult>( schema: TSchema, handler: (data: z.infer<TSchema>) => Promise<TResult> ) { return async (formData: FormData): Promise<ActionResult<TResult>> => { const rawData = Object.fromEntries(formData.entries()); const result = schema.safeParse(rawData); if (!result.success) { return { success: false, errors: result.error.flatten().fieldErrors as Record<string, string[]>, }; } const data = await handler(result.data); return { success: true, data }; }; } // Usage: export const createPost = createValidatedAction(CreatePostSchema, async (data) => { return db.post.create({ data }); });
Details
FormData coercion:
All form values come in as strings. Use
z.coerce for non-string fields: z.coerce.number(), z.coerce.boolean(), z.coerce.date(). Checkbox values are 'on' or absent — coerce with:
const boolField = z.preprocess( (val) => val === 'on' || val === 'true' || val === true, z.boolean() );
File inputs:
const FileSchema = z.object({ file: z .instanceof(File) .refine((f) => f.size < 5 * 1024 * 1024, 'File must be under 5MB') .refine((f) => ['image/jpeg', 'image/png'].includes(f.type), 'Must be JPEG or PNG'), });
tRPC integration:
In tRPC procedures, pass Zod schemas directly to
.input():
const postRouter = router({ create: protectedProcedure.input(CreatePostSchema).mutation(({ ctx, input }) => { // input is fully typed as z.infer<typeof CreatePostSchema> return ctx.db.post.create({ data: input }); }), });
Source
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.