Skip to main content
skill CX Codex

zod · bobmatnyc — Claude Code skill: schema validation and types

A skill about the zod library: define a schema once and get runtime checks plus TypeScript types. Useful for forms and API payloads.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add bobmatnyc/claude-mpm-skills --skill zod -a claude-code
Open on GitHub

What zod does

The skill covers zod, a validation library where you write a schema once and pull a TypeScript type out of it with z.infer. It walks through primitives, objects and arrays, validation methods, schema composition, transformations, error handling and async validation. Separate sections deal with API handler patterns and integrations.

The content is split into sections with token estimates. The entry point is small; the integrations section is the longest by a wide margin. So the skill loads the part you need instead of the whole file. Handy when zod is already in the project and you want a quick reminder of how safeParse differs from parse.

One thing to know: the skill is marked user-invocable: false and disable-model-invocation: true. It never fires on its own, neither from a slash command nor from a prompt. It is reference material you read while writing code. If your project validates with something else, this skill does nothing for you.

What it helps with

  • derive a TypeScript type from a schema with z.infer
  • validate a request body inside an API handler
  • read field-level errors from result.error.issues
  • describe a nested object with address and contacts
  • set default values for a settings object

Example prompts for the agent

  • show me how to validate a request body with zod and return field errors
  • I need a user schema with email, age of at least 18 and a role from an enum
  • how do I transform a date string into a Date object in zod and get both types

How to install zod

1Skills CLI (Vercel)

npx skills add bobmatnyc/claude-mpm-skills --skill zod -a claude-code

2Manually: put the files in place

~/.claude/skills/zod/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/zod/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about zod

How do I install the zod skill in Claude?

Run npx skills add bobmatnyc/claude-mpm-skills --skill zod -a claude-code. The skill file lands at ~/.claude/skills/zod/SKILL.md.

What does the zod skill do?

It documents the zod library: primitives, objects, arrays, type inference, validation methods, transformations and error handling. The material is split into sections that load on demand.

Can I invoke this skill manually?

No. It sets user-invocable: false and disable-model-invocation: true, so it acts as reference material while you write code rather than a command.

Source text

SKILL.md · MIT
---
name: zod
description: TypeScript-first schema validation library with static type inference for form validation, API validation, and runtime type checking with compile-time types.
user-invocable: false
disable-model-invocation: true
progressive_disclosure:
  entry_point:
    - summary
    - when_to_use
    - quick_start
  sections:
    - primitives
    - objects_and_arrays
    - type_inference
    - validation_methods
    - schema_composition
    - transformations
    - error_handling
    - async_validation
    - advanced_types
    - api_handler_patterns
    - integrations
    - best_practices
token_estimates:
  entry: 65
  primitives: 200
  objects_and_arrays: 300
  type_inference: 150
  validation_methods: 250
  schema_composition: 300
  transformations: 400
  error_handling: 250
  async_validation: 200
  advanced_types: 500
  api_handler_patterns: 800
  integrations: 1200
  best_practices: 300
  full: 5800
---

# Zod Validation Skill

## Summary
TypeScript-first schema validation library with static type inference. Define schemas once, get runtime validation and compile-time types automatically.

## When to Use
- Form validation with type-safe data
- API request/response validation
- Environment variable validation
- Runtime type checking with TypeScript inference
- tRPC procedure inputs/outputs
- Database schema validation (Drizzle, Prisma)

## Quick Start

```typescript
import { z } from 'zod';

// Define schema
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().min(18),
  role: z.enum(['user', 'admin'])
});

// Infer TypeScript type
type User = z.infer<typeof UserSchema>;

// Validate data
const result = UserSchema.safeParse(data);
if (result.success) {
  const user: User = result.data;
}
```

<!-- SECTION: primitives -->
## Primitive Types

### Basic Types

```typescript
import { z } from 'zod';

// String with validation
const nameSchema = z.string()
  .min(2, "Too short")
  .max(50, "Too long")
  .trim();

const emailSchema = z.string().email();
const urlSchema = z.string().url();
const uuidSchema = z.string().uuid();
const regexSchema = z.string().regex(/^[A-Z]{3}$/);

// Numbers
const ageSchema = z.number()
  .int("Must be integer")
  .positive()
  .min(0)
  .max(120);

const priceSchema = z.number()
  .positive()
  .multipleOf(0.01); // Currency precision

// Boolean
const isActiveSchema = z.boolean();

// Date
const createdAtSchema = z.date()
  .min(new Date('2020-01-01'))
  .max(new Date());

const dateStringSchema = z.string().datetime(); // ISO 8601
const dateOnlySchema = z.string().date(); // YYYY-MM-DD
```

### Special Types

```typescript
// Literal values
const roleSchema = z.literal('admin');
const statusSchema = z.literal('pending');

// Enums
const ColorEnum = z.enum(['red', 'green', 'blue']);
type Color = z.infer<typeof ColorEnum>; // 'red' | 'green' | 'blue'

const NativeEnum = z.nativeEnum(MyEnum); // For TypeScript enums

// Nullable and Optional
const optionalString = z.string().optional(); // string | undefined
const nullableString = z.string().nullable(); // string | null
const nullishString = z.string().nullish(); // string | null | undefined

// Default values
const countSchema = z.number().default(0);
const settingsSchema = z.object({
  theme: z.string().default('light'),
  notifications: z.boolean().default(true)
});
```

<!-- SECTION: objects_and_arrays -->
## Objects and Arrays

### Object Schemas

```typescript
// Basic object
const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  name: z.string(),
  age: z.number().optional()
});

// Nested objects
const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  country: z.string(),
  zipCode: z.string()
});

const PersonSchema = z.object({
  name: z.string(),
  address: AddressSchema,
  contacts: z.object({
    email: z.string().email(),
    phone: z.string().optional()
  })
});

// Strict vs Passthrough
const strictSchema = z.object({ name: z.string() }).strict();
// Rejects unknown keys

const passthroughSchema = z.object({ name: z.string() }).passthrough();
// Allows unknown keys

const stripSchema = z.object({ name: z.string() }).strip();
// Removes unknown keys (default)
```

### Array Schemas

```typescript
// Simple arrays
const stringArray = z.array(z.string());
const numberArray = z.array(z.number()).min(1).max(10);

// Array of objects
const UsersSchema = z.array(UserSchema);

// Non-empty arrays
const tagSchema = z.array(z.string()).nonempty("At least one tag required");

// Fixed-length arrays (tuples)
const coordinateSchema = z.tuple([z.number(), z.number()]);
type Coordinate = z.infer<typeof coordinateSchema>; // [number, number]

// Tuple with rest
const csvRowSchema = z.tuple([z.string(), z.number()]).rest(z.string());
// [string, number, ...string[]]
```

### Records and Maps

```typescript
// Record (object with dynamic keys)
const userRolesSchema = z.record(
  z.string(), // key type
  z.enum(['admin', 'user', 'guest']) // value type
);
type UserRoles = z.infer<typeof userRolesSchema>;
// { [key: string]: 'admin' | 'user' | 'guest' }

// Map
const configMapSchema = z.map(
  z.string(), // key
  z.number()  // value
);

// Set
const uniqueTagsSchema = z.set(z.string());
```

<!-- SECTION: type_inference -->
## Type Inference

```typescript
import { z } from 'zod';

// Infer output type
const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  age: z.number()
});

type User = z.infer<typeof UserSchema>;
// { id: string; email: string; age: number }

// Infer input type (before transforms)
const TransformSchema = z.object({
  date: z.string().transform(s => new Date(s))
});

type Input = z.input<typeof TransformSchema>;
// { date: string }

type Output = z.output<typeof TransformSchema>;
// { date: Date }

// Using inferred types in functions
function createUser(data: User): void {
  // data is type-safe
}

function validateAndCreate(data: unknown): User | null {
  const result = UserSchema.safeParse(data);
  return result.success ? result.data : null;
}
```

<!-- SECTION: validation_methods -->
## Validation Methods

### Parse vs SafeParse

```typescript
// parse() - Throws on failure
try {
  const user = UserSchema.parse(data);
  // user is type User
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error(error.issues);
  }
}

// safeParse() - Returns result object
const result = UserSchema.safeParse(data);

if (result.success) {
  const user = result.data; // type User
} else {
  const errors = result.error.issues;
  errors.forEach(err => {
    console.log(`${err.path}: ${err.message}`);
  });
}

// parseAsync() - For async refinements
const asyncResult = await UserSchema.parseAsync(data);

// safeParseAsync() - Safe async version
const asyncSafeResult = await UserSchema.safeParseAsync(data);
```

### Partial Validation

```typescript
// Check if data matches schema without throwing
const isValid = UserSchema.safeParse(data).success;

// Custom type guards
function isUser(data: unknown): data is User {
  return UserSchema.safeParse(data).success;
}

if (isUser(unknownData)) {
  // TypeScript knows unknownData is User
  console.log(unknownData.email);
}
```

<!-- SECTION: schema_composition -->
## Schema Composition

### Extending and Merging

```typescript
// Extend (add fields)
const BaseUserSchema = z.object({
  id: z.string(),
  email: z.string()
});

const AdminUserSchema = BaseUserSchema.extend({
  role: z.literal('admin'),
  permissions: z.array(z.string())
});

// Merge (combine schemas)
const NameSchema = z.object({ name: z.string() });
const AgeSchema = z.object({ age: z.number() });

const PersonSchema = NameSchema.merge(AgeSchema);
// { name: string; age: number }

// Pick (select fields)
const UserIdEmail = UserSchema.pick({ id: true, email: true });

// Omit (exclude fields)
const UserWithoutId = UserSchema.omit({ id: true });

// Partial (make all fields optional)
const PartialUser = UserSchema.partial();

// DeepPartial (recursive partial)
const DeepPartialUser = UserSchema.deepPartial();

// Required (make all fields required)
const RequiredUser = UserSchema.required();
```

### Union and Intersection

```typescript
// Union (OR)
const StringOrNumber = z.union([z.string(), z.number()]);
// Shorthand
const StringOrNumberAlt = z.string().or(z.number());

// Discriminated Union (tagged union)
const SuccessResponse = z.object({
  status: z.literal('success'),
  data: z.any()
});

const ErrorResponse = z.object({
  status: z.literal('error'),
  message: z.string()
});

const ApiResponse = z.discriminatedUnion('status', [
  SuccessResponse,
  ErrorResponse
]);

// Intersection (AND)
const User = z.object({ name: z.string() });
const Timestamps = z.object({
  createdAt: z.date(),
  updatedAt: z.date()
});

const UserWithTimestamps = z.intersection(User, Timestamps);
// Shorthand
const UserWithTimestampsAlt = User.and(Timestamps);
```

<!-- SECTION: transformations -->
## Transformations and Refinements

### Transform

```typescript
// Transform data after validation
const StringToNumber = z.string().transform(val => parseInt(val, 10));

const DateSchema = z.string().transform(str => new Date(str));

// Chaining transforms
const TrimmedLowercase = z.string()
  .transform(s => s.trim())
  .transform(s => s.toLowerCase());

// Transform with validation
const PositiveStringNumber = z.string()
  .transform(val => parseInt(val, 10))
  .refine(n => n > 0, "Must be positive");

// Complex transformations
const UserInputSchema = z.object({
  name: z.string().transform(s => s.trim()),
  email: z.string().email().transform(s => s.toLowerCase()),
  birthDate: z.string().transform(s => new Date(s)),
  tags: z.string().transform(s => s.split(',').map(t => t.trim()))
});

type UserInput = z.input<typeof UserInputSchema>;
// { name: string; email: string; birthDate: string; tags: string }

type User = z.output<typeof UserInputSchema>;
// { name: string; email: string; birthDate: Date; tags: string[] }
```

### Refine (Custom Validation)

```typescript
// Simple refinement
const PasswordSchema = z.string()
  .min(8)
  .refine(
    val => /[A-Z]/.test(val),
    "Must contain uppercase letter"
  )
  .refine(
    val => /[0-9]/.test(val),
    "Must contain number"
  );

// Refinement with custom error
const UniqueEmailSchema = z.string().email().refine(
  async (email) => {
    const exists = await checkEmailExists(email);
    return !exists;
  },
  { message: "Email already taken" }
);

// Object-level refinement
const PasswordMatchSchema = z.object({
  password: z.string(),
  confirmPassword: z.string()
}).refine(
  data => data.password === data.confirmPassword,
  {
    message: "Passwords don't match",
    path: ["confirmPassword"] // Error location
  }
);

// Multiple field validation
const DateRangeSchema = z.object({
  startDate: z.date(),
  endDate: z.date()
}).refine(
  data => data.endDate > data.startDate,
  {
    message: "End date must be after start date",
    path: ["endDate"]
  }
);
```

### SuperRefine (Advanced)

```typescript
// Access to Zod context for complex validation
const ComplexSchema = z.object({
  type: z.enum(['email', 'phone']),
  value: z.string()
}).superRefine((data, ctx) => {
  if (data.type === 'email') {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(data.value)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Invalid email format",
        path: ["value"]
      });
    }
  } else if (data.type === 'phone') {
    const phoneRegex = /^\+?[1-9]\d{1,14}$/;
    if (!phoneRegex.test(data.value)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Invalid phone format",
        path: ["value"]
      });
    }
  }
});

// Multiple issues
const RegistrationSchema = z.object({
  username: z.string(),
  email: z.string(),
  age: z.number()
}).superRefine(async (data, ctx) => {
  // Check username availability
  if (await usernameTaken(data.username)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Username taken",
      path: ["username"]
    });
  }

  // Check email availability
  if (await emailTaken(data.email)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Email already registered",
      path: ["email"]
    });
  }

  // Age restriction
  if (data.age < 18) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Must be 18 or older",
      path: ["age"]
    });
  }
});
```

<!-- SECTION: error_handling -->
## Error Handling

### Custom Error Messages

```typescript
// Field-level messages
const UserSchema = z.object({
  email: z.string().email({ message: "Invalid email address" }),
  age: z.number({
    required_error: "Age is required",
    invalid_type_error: "Age must be a number"
  }).min(18, { message: "Must be 18 or older" })
});

// Global error map
import { z } from 'zod';

const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
  if (issue.code === z.ZodIssueCode.invalid_type) {
    if (issue.expected === "string") {
      return { message: "This field must be text" };
    }
  }
  if (issue.code === z.ZodIssueCode.too_small) {
    if (issue.type === "string") {
      return { message: `Minimum ${issue.minimum} characters required` };
    }
  }
  return { message: ctx.defaultError };
};

z.setErrorMap(customErrorMap);
```

### Processing Errors

```typescript
// Flatten errors for forms
const result = UserSchema.safeParse(data);

if (!result.success) {
  const flatErrors = result.error.flatten();

  console.log(flatErrors.formErrors); // Top-level errors
  console.log(flatErrors.fieldErrors);
  // { email: ["Invalid email"], age: ["Must be 18+"] }
}

// Format for API response
function formatZodError(error: z.ZodError) {
  return error.issues.map(issue => ({
    field: issue.path.join('.'),
    message: issue.message
  }));
}

// Example usage
const result = UserSchema.safeParse(data);
if (!result.success) {
  return res.status(400).json({
    errors: formatZodError(result.error)
  });
}
```

<!-- SECTION: async_validation -->
## Async Validation

```typescript
import { z } from 'zod';

// Async refinement
const UsernameSchema = z.string().refine(
  async (username) => {
    const available = await checkUsernameAvailable(username);
    return available;
  },
  { message: "Username already taken" }
);

// Must use parseAsync or safeParseAsync
const result = await UsernameSchema.safeParseAsync("john_doe");

// Complex async validation
const RegistrationSchema = z.object({
  username: z.string().refine(
    async (val) => !(await usernameTaken(val)),
    "Username taken"
  ),
  email: z.string().email().refine(
    async (val) => !(await emailTaken(val)),
    "Email already registered"
  ),
  inviteCode: z.string().refine(
    async (code) => await validateInviteCode(code),
    "Invalid invite code"
  )
});

// Validate
const userData = await RegistrationSchema.parseAsync(input);

// With error handling
const result = await RegistrationSchema.safeParseAsync(input);
if (!result.success) {
  // Handle validation errors
}
```

<!-- SECTION: advanced_types -->
## Advanced Types

### Recursive Types

```typescript
// Self-referential schemas
type Category = {
  name: string;
  subcategories: Category[];
};

const CategorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    subcategories: z.array(CategorySchema)
  })
);

// Tree structure
type TreeNode = {
  value: number;
  left?: TreeNode;
  right?: TreeNode;
};

const TreeNodeSchema: z.ZodType<TreeNode> = z.lazy(() =>
  z.object({
    value: z.number(),
    left: TreeNodeSchema.optional(),
    right: TreeNodeSchema.optional()
  })
);
```

### Discriminated Unions

```typescript
// Type-safe union based on discriminator field
const Circle = z.object({
  kind: z.literal('circle'),
  radius: z.number()
});

const Rectangle = z.object({
  kind: z.literal('rectangle'),
  width: z.number(),
  height: z.number()
});

const Triangle = z.object({
  kind: z.literal('triangle'),
  base: z.number(),
  height: z.number()
});

const Shape = z.discriminatedUnion('kind', [
  Circle,
  Rectangle,
  Triangle
]);

type Shape = z.infer<typeof Shape>;

// TypeScript can narrow based on discriminator
function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
  }
}
```

### Preprocess

```typescript
// Transform before validation
const NumberFromString = z.preprocess(
  (val) => (typeof val === 'string' ? parseInt(val, 10) : val),
  z.number()
);

// Clean data before validation
const TrimmedString = z.preprocess(
  (val) => (typeof val === 'string' ? val.trim() : val),
  z.string()
);

// Parse JSON strings
const JsonSchema = z.preprocess(
  (val) => (typeof val === 'string' ? JSON.parse(val) : val),
  z.object({
    name: z.string(),
    age: z.number()
  })
);

// Form data preprocessing
const FormDataSchema = z.preprocess(
  (data) => {
    // Convert FormData to object
    if (data instanceof FormData) {
      return Object.fromEntries(data.entries());
    }
    return data;
  },
  z.object({
    name: z.string(),
    email: z.string().email()
  })
);
```

### Branded Types

```typescript
// Create nominal types
const UserId = z.string().uuid().brand<'UserId'>();
type UserId = z.infer<typeof UserId>;

const Email = z.string().email().brand<'Email'>();
type Email = z.infer<typeof Email>;

// Prevents mixing similar types
function getUserById(id: UserId) { /* ... */ }
function sendEmail(to: Email) { /* ... */ }

const userId = UserId.parse('123e4567-e89b-12d3-a456-426614174000');
const email = Email.parse('[email protected]');

getUserById(userId); // ✓
getUserById(email);  // ✗ Type error
```

<!-- SECTION: api_handler_patterns -->
## API Handler Patterns

### Generic Validated Handler

Create type-safe API handlers with automatic validation, error formatting, and authentication.

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';

// Generic handler type with validated input
type ValidatedHandler<T, C = T> = (
  state: {
    input: T;
    request: NextRequest;
    ctx: RouteContext<C>;
  }
) => Promise<Response> | Response;

// Route context interface
interface RouteContext<T = unknown> {
  params: T;
}

// Validation configuration
interface ValidationConfig<T extends z.ZodType, C extends z.ZodType> {
  input?: {
    schema: T;
    source: 'body' | 'query' | 'params';
  };
  auth?: {
    role: 'admin' | 'provider' | 'user';
  };
  context?: C;
}

// Generic validated handler wrapper
export function validatedHandler<
  T extends z.ZodType,
  C extends z.ZodType = z.ZodType<any>
>(
  config: ValidationConfig<T, C>,
  handler: ValidatedHandler<z.infer<T>, z.infer<C>>
) {
  return async (
    request: NextRequest,
    ctx: RouteContext<z.infer<C>>
  ): Promise<Response> => {
    try {
      // Extract input based on source
      let rawInput: unknown;

      if (config.input) {
        switch (config.input.source) {
          case 'body':
            rawInput = await request.json();
            break;
          case 'query':
            rawInput = Object.fromEntries(request.nextUrl.searchParams);
            break;
          case 'params':
            rawInput = ctx.params;
            break;
        }

        // Validate input
        const result = config.input.schema.safeParse(rawInput);

        if (!result.success) {
          return NextResponse.json(
            {
              error: 'Validation failed',
              details: result.error.issues.map(err => ({
                path: err.path.join('.'),
                message: err.message,
                code: err.code,
              })),
            },
            { status: 400 }
          );
        }

        // Call handler with validated input
        return handler({
          input: result.data,
          request,
          ctx
        });
      }

      // No validation required
      return handler({
        input: {} as z.infer<T>,
        request,
        ctx
      });

    } catch (error) {
      console.error('Handler error:', error);
      return NextResponse.json(
        { error: 'Internal server error' },
        { status: 500 }
      );
    }
  };
}
```

**Usage Example:**

```typescript
// Define validation schema
const CreateCampSchema = z.object({
  name: z.string().min(3),
  location: z.string(),
  startDate: z.string().datetime(),
  endDate: z.string().datetime(),
  capacity: z.number().positive(),
});

// Type-safe route handler
export const POST = validatedHandler(
  {
    input: {
      schema: CreateCampSchema,
      source: 'body',
    },
    auth: { role: 'admin' },
  },
  async ({ input, request }) => {
    // input is fully typed as z.infer<typeof CreateCampSchema>
    const camp = await createCamp({
      name: input.name,
      location: input.location,
      startDate: new Date(input.startDate),
      endDate: new Date(input.endDate),
      capacity: input.capacity,
    });

    return NextResponse.json(camp, { status: 201 });
  }
);
```

### Discriminated Unions for Complex Types

Use discriminated unions to handle multiple input types with type-safe narrowing.

```typescript
// Location selection with discriminated union
const LocationSelectionSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('map-bounds'),
    north: z.number().min(-90).max(90),
    south: z.number().min(-90).max(90),
    east: z.number().min(-180).max(180),
    west: z.number().min(-180).max(180),
  }),
  z.object({
    type: z.literal('address'),
    address: z.string().min(1),
    radius: z.number().positive().optional(),
  }),
  z.object({
    type: z.literal('user-location'),
    lat: z.number().min(-90).max(90),
    lng: z.number().min(-180).max(180),
    radius: z.number().positive().default(10),
  }),
]);

type LocationSelection = z.infer<typeof LocationSelectionSchema>;

// Type-safe handler with union narrowing
function processLocation(selection: LocationSelection) {
  switch (selection.type) {
    case 'map-bounds':
      return searchByBounds({
        north: selection.north,
        south: selection.south,
        east: selection.east,
        west: selection.west,
      });

    case 'address':
      return searchByAddress({
        address: selection.address,
        radius: selection.radius,
      });

    case 'user-location':
      return searchByCoordinates({
        lat: selection.lat,
        lng: selection.lng,
        radius: selection.radius,
      });
  }
}
```

### Complete Type Mapping with Required

Ensure all keys are mapped with TypeScript's `Required` utility type.

```typescript
import { NumberParam, StringParam, DateParam } from 'next-query-params';

// Filters interface
interface Filters {
  startDate?: Date;
  endDate?: Date;
  status?: 'active' | 'inactive' | 'pending';
  search?: string;
  minPrice?: number;
  maxPrice?: number;
}

// Query param config interface
interface QueryParamConfig<T> {
  encode: (value: T) => string;
  decode: (value: string | undefined) => T | undefined;
}

// Complete mapping enforced by Required<Filters>
const filtersQueryParamConfigMap: {
  [Key in keyof Required<Filters>]: QueryParamConfig<Filters[Key]>;
} = {
  startDate: DateParam,
  endDate: DateParam,
  status: StringParam,
  search: StringParam,
  minPrice: NumberParam,
  maxPrice: NumberParam,
  // TypeScript error if any key is missing
};

// Zod schema matching the interface
const FiltersSchema = z.object({
  startDate: z.date().optional(),
  endDate: z.date().optional(),
  status: z.enum(['active', 'inactive', 'pending']).optional(),
  search: z.string().optional(),
  minPrice: z.number().positive().optional(),
  maxPrice: z.number().positive().optional(),
});

// Validation with cross-field checks
const ValidatedFiltersSchema = FiltersSchema.refine(
  (data) => {
    if (data.startDate && data.endDate) {
      return data.endDate >= data.startDate;
    }
    return true;
  },
  {
    message: 'End date must be after start date',
    path: ['endDate'],
  }
).refine(
  (data) => {
    if (data.minPrice && data.maxPrice) {
      return data.maxPrice >= data.minPrice;
    }
    return true;
  },
  {
    message: 'Max price must be greater than min price',
    path: ['maxPrice'],
  }
);
```

### Structured Error Response Format

Create consistent, type-safe error responses.

```typescript
// Error response schema
const ApiErrorSchema = z.object({
  code: z.enum([
    'VALIDATION_ERROR',
    'AUTHENTICATION_ERROR',
    'AUTHORIZATION_ERROR',
    'NOT_FOUND',
    'INTERNAL_ERROR',
  ]),
  message: z.string(),
  details: z.array(
    z.object({
      path: z.string(),
      message: z.string(),
      code: z.string().optional(),
    })
  ).optional(),
  timestamp: z.string().datetime(),
});

type ApiError = z.infer<typeof ApiErrorSchema>;

// Format Zod errors for API response
function formatZodError(error: z.ZodError): ApiError {
  return {
    code: 'VALIDATION_ERROR',
    message: 'Input validation failed',
    details: error.issues.map(issue => ({
      path: issue.path.join('.'),
      message: issue.message,
      code: issue.code,
    })),
    timestamp: new Date().toISOString(),
  };
}

// Error response helper
function errorResponse(
  code: ApiError['code'],
  message: string,
  status: number,
  details?: ApiError['details']
): Response {
  const error: ApiError = {
    code,
    message,
    details,
    timestamp: new Date().toISOString(),
  };

  return NextResponse.json(error, { status });
}

// Usage in handler
export const POST = validatedHandler(
  {
    input: { schema: CreateUserSchema, source: 'body' },
  },
  async ({ input }) => {
    try {
      const user = await createUser(input);
      return NextResponse.json(user, { status: 201 });
    } catch (error) {
      if (error instanceof z.ZodError) {
        return NextResponse.json(
          formatZodError(error),
          { status: 400 }
        );
      }

      return errorResponse(
        'INTERNAL_ERROR',
        'Failed to create user',
        500
      );
    }
  }
);
```

### Query Parameter Transformation

Handle query parameter parsing with validation and transformation.

```typescript
// Query param schema with transforms
const SearchParamsSchema = z.object({
  // String to number with validation
  page: z
    .string()
    .transform(Number)
    .pipe(z.number().int().positive().default(1)),

  // String to number with limits
  pageSize: z
    .string()
    .transform(Number)
    .pipe(z.number().int().min(1).max(100).default(20)),

  // CSV string to array
  tags: z
    .string()
    .optional()
    .transform(val => val ? val.split(',').map(t => t.trim()) : []),

  // ISO date string to Date
  startDate: z
    .string()
    .datetime()
    .optional()
    .transform(val => val ? new Date(val) : undefined),

  // Boolean string to boolean
  includeInactive: z
    .string()
    .optional()
    .transform(val => val === 'true')
    .pipe(z.boolean().default(false)),

  // Enum validation
  sortBy: z
    .enum(['name', 'date', 'price'])
    .default('date'),

  // Sort order
  sortOrder: z
    .enum(['asc', 'desc'])
    .default('desc'),
});

// Handler with query param validation
export const GET = validatedHandler(
  {
    input: {
      schema: SearchParamsSchema,
      source: 'query',
    },
  },
  async ({ input }) => {
    // All params are properly typed and transformed
    const results = await searchItems({
      page: input.page,              // number
      pageSize: input.pageSize,      // number
      tags: input.tags,              // string[]
      startDate: input.startDate,    // Date | undefined
      includeInactive: input.includeInactive, // boolean
      sortBy: input.sortBy,          // 'name' | 'date' | 'price'
      sortOrder: input.sortOrder,    // 'asc' | 'desc'
    });

    return NextResponse.json(results);
  }
);
```

### Schema Composition for Reusable Validation

Build complex schemas from reusable parts.

```typescript
// Base schemas
const TimestampSchema = z.object({
  createdAt: z.date(),
  updatedAt: z.date(),
});

const PaginationSchema = z.object({
  page: z.number().int().positive(),
  pageSize: z.number().int().min(1).max(100),
  total: z.number().int().nonnegative(),
});

const LocationSchema = z.object({
  lat: z.number().min(-90).max(90),
  lng: z.number().min(-180).max(180),
  address: z.string().optional(),
});

// Composed schemas
const CampSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(3),
  location: LocationSchema,
  capacity: z.number().positive(),
  status: z.enum(['active', 'inactive', 'full']),
}).merge(TimestampSchema);

const CampListResponseSchema = z.object({
  camps: z.array(CampSchema),
  pagination: PaginationSchema,
});

// Type inference works across composition
type Camp = z.infer<typeof CampSchema>;
type CampListResponse = z.infer<typeof CampListResponseSchema>;

// Usage in handler
export const GET = validatedHandler(
  {
    input: {
      schema: z.object({
        page: z.number().default(1),
        pageSize: z.number().default(20),
      }),
      source: 'query',
    },
  },
  async ({ input }) => {
    const camps = await getCamps(input.page, input.pageSize);

    // Response validated against schema
    const response: CampListResponse = {
      camps,
      pagination: {
        page: input.page,
        pageSize: input.pageSize,
        total: await getCampCount(),
      },
    };

    return NextResponse.json(response);
  }
);
```

<!-- SECTION: integrations -->
## Integrations

### React Hook Form

```typescript
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const FormSchema = z.object({
  username: z.string().min(3, "Minimum 3 characters"),
  email...

© bobmatnyc · MIT · bobmatnyc/claude-mpm-skills

Similar skills

All skills for Programming languages →

zod by other authors comes first.

zod PyModel A set of 46 validation and type inference rules for Zod 4 in TypeScript projects. Helps agents write schemas, handle errors, and clean up deprecated syntax. CX Codex +1 476installs zod Anivar Aravind A rule set for Zod v4: parsing untrusted data at boundaries, error handling, type inference, and the v3 migration. Useful when the agent writes schemas from memory and mixes up the API. CX Codex +1 1,000installs zod-4 prowler-cloud Provides validation patterns for Zod 4 and guides migration from Zod 3. It helps write schemas for forms, API payloads, and adapters. CX Codex +1 1,200installs zod-validation-utilities giuseppe-trisciuoglio Guides Zod v4 validation for TypeScript APIs, forms, and configuration. Use it when external input needs parsing, coercion, and clear validation errors. CX Codex +1 3,800installs zod-schema-validation Mindrally Guides agents on writing Zod schemas, inferring TypeScript types, and validating boundaries in APIs and UI forms. Keeps validation consistent across the codebase. CX Codex +1 1,200installs react-hook-form-zod ovachiever A skill that walks your agent through React Hook Form with Zod validation. One schema covers the client and the server. CX Codex +1 722installs valibot open-circle Teaches the agent to work with Valibot: build schemas, parse input, and stop mixing the API up with Zod. Useful when the library is already in your project. CX Codex +1 2,100installs express-typescript Mindrally Rules that steer your agent when it writes Express.js and TypeScript APIs: project layout, middleware, validation, security. CX Codex +1 1,400installs

From the same set65

drizzle-migrations A skill that puts SQL migrations first in TypeScript projects on Drizzle ORM. Useful when the same schema has to hold across several environments. CX Codex +1 1,300installs docker A Docker skill that helps your agent write Dockerfiles, build images and run services with Compose. Useful when the same app has to behave the same in dev and production. CX Codex +1 1,000installs biome A skill that tells the agent how to run Biome on your project: linting, formatting and import sorting from a single command. CX Codex +1 927installs cypress A skill for writing Cypress E2E and component tests: stable selectors, deterministic waits and network stubbing instead of fixed sleeps. CX Codex +1 653installs session-compression A skill with techniques for compressing multi-turn conversations: summarization, embedding-based retrieval, context management. Useful when chat history nears the context limit. CX Codex +1 642installs opentelemetry A skill with OpenTelemetry patterns: traces, metrics, logs, context propagation and Collector pipelines. Useful when a service needs instrumenting instead of guesswork. CX Codex +1 635installs xlsx A skill that shows the agent how to read, write and edit Excel files using openpyxl, pandas and the JavaScript xlsx library. CX Codex +1 588installs wordpress-security-validation A skill for safe WordPress plugin and theme development: sanitize input, validate logic, escape output. Useful when handling forms, AJAX, or rendering untrusted content. CX Codex +1 581installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor