Harness-engineering zod-nextjs-integration

Zod Next.js Integration

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.md
source 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
    useFormState
    /
    useActionState
  • Validating search params and dynamic route segments

Instructions

  1. Validate server action inputs — the action receives
    FormData
    or typed arguments:
'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}`);
}
  1. Use
    useActionState
    (React 19) or
    useFormState
    with typed state:
'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>
  )
}
  1. 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 });
}
  1. 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
}
  1. 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 } });
  // ...
}
  1. 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

https://zod.dev

Process

  1. Read the instructions and examples in this document.
  2. Apply the patterns to your implementation, adapting to your specific context.
  3. 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.