К основному содержанию
скилл CX Codex

zod · bobmatnyc — скилл для Claude Code: валидация схем и типов

Скилл по библиотеке zod: схемы один раз, а на выходе проверка данных в рантайме и типы TypeScript. Пригодится на формах и API.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add bobmatnyc/claude-mpm-skills --skill zod -a claude-code
Открыть на GitHub

Что делает zod

Скилл описывает работу с zod — библиотекой валидации, где схема задаётся один раз и из неё выводится тип TypeScript через z.infer. Внутри разбор примитивов, объектов и массивов, методов проверки, композиции схем, трансформаций, обработки ошибок и асинхронной валидации. Отдельные разделы посвящены шаблонам для обработчиков API и интеграциям.

Материал разложен по секциям с оценкой объёма: входная часть небольшая, а раздел про интеграции заметно длиннее остальных. Поэтому скилл подгружает нужный кусок по мере обращения, а не тянет весь текст сразу. Полезно, когда в проекте уже стоит zod и надо быстро вспомнить, чем safeParse отличается от parse.

Учтите: скилл помечен как user-invocable: false и disable-model-invocation: true. Сам по себе он не запускается ни по слэш-команде, ни по запросу — это справочник, который читается при работе с кодом. Если в проекте другой валидатор, толку от него не будет.

Какие задачи решает

  • вывести тип TypeScript из схемы через z.infer
  • проверить тело запроса на входе в API-обработчик
  • разобрать ошибки валидации по полям через result.error.issues
  • описать вложенный объект с адресом и контактами
  • задать значения по умолчанию для настроек

Примеры запросов агенту

  • покажи, как через zod провалидировать тело запроса и вернуть ошибки по полям
  • нужна схема пользователя с email, возрастом от 18 и ролью из enum
  • как в zod сделать трансформацию строки даты в объект Date и вывести оба типа

Как установить zod

1Через Skills CLI (Vercel)

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

2Вручную: положить файлы

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

Папка скилла целиком; для одного проекта — .claude/skills/zod/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Из того же набора65

drizzle-migrations Скилл задаёт порядок работы с базой в TypeScript: сначала SQL-миграция, потом схема. Пригодится, когда схема меняется в нескольких окружениях. CX Codex +1 1 300установок docker Скилл по Docker: помогает агенту писать Dockerfile, собирать образы и поднимать сервисы через Compose. Пригодится, когда проект надо упаковать одинаково для разработки и продакшена. CX Codex +1 1 000установок biome Скилл подсказывает агенту, как прогнать проект через Biome: линтинг, форматирование и сортировка импортов одной командой. CX Codex +1 927установок cypress Скилл для написания E2E- и компонентных тестов на Cypress: устойчивые селекторы, ожидания без фиксированных пауз, перехват сети. CX Codex +1 653установок session-compression Скилл с приёмами сжатия многошаговых диалогов: суммаризация, поиск по эмбеддингам, управление контекстом. Пригодится, когда история чата подбирается к лимиту окна. CX Codex +1 642установок opentelemetry Скилл с паттернами OpenTelemetry: трейсы, метрики, логи, проброс контекста и пайплайны Collector. Пригодится, когда сервис пора инструментировать, а не гадать по логам. CX Codex +1 635установок xlsx Скилл для чтения, записи и правки Excel-файлов из кода: openpyxl, pandas и библиотека xlsx для JavaScript. CX Codex +1 588установок wordpress-security-validation Скилл для безопасной разработки плагинов и тем WordPress: санитизация ввода, проверка логики и экранирование вывода. Пригодится, когда обрабатываете формы, AJAX или выводите чужие данные. CX Codex +1 581установок

Другие версии zod

Похожие

zod-4 prowler-cloud Скилл обучает ассистента правилам валидации Zod 4 и миграции со старых версий. Он нужен при создании схем для форм, парсинга запросов и адаптеров. CX Codex +1 1 200установок zod-validation-utilities giuseppe-trisciuoglio Помогает писать схемы Zod v4 для проверки API, форм и конфигурации в TypeScript. Пригодится для разбора внешних данных и понятной обработки ошибок. CX Codex +1 3 800установок zod-schema-validation Mindrally Скилл задаёт правила валидации данных через Zod и вывода типов TypeScript на границах системы: в API, формах и внешних ответах. Помогает писать схемы без лишнего дублирования. CX Codex +1 1 200установок react-hook-form-zod ovachiever Скилл учит агента собирать формы на React Hook Form с валидацией через Zod. Одна схема работает и на клиенте, и на сервере. CX Codex +1 722установок valibot open-circle Скилл учит агента работать с Valibot: создавать схемы, парсить ввод и не путать синтаксис с Zod. Пригодится, когда в проекте уже стоит эта библиотека. CX Codex +1 2 100установок express-typescript Mindrally Правила для агента, который пишет API на Express.js и TypeScript: структура проекта, middleware, валидация и безопасность. CX Codex +1 1 400установок

Рядом в теме «Языки программирования»

Все скиллы по теме «Языки программирования» →

Вопросы о zod

Как установить zod в Claude?

Выполните npx skills add bobmatnyc/claude-mpm-skills --skill zod -a claude-code. Файл скилла окажется по пути ~/.claude/skills/zod/SKILL.md.

Что делает скилл zod?

Описывает библиотеку zod: примитивы, объекты, массивы, вывод типов, методы проверки, трансформации и обработку ошибок. Материал разбит на секции, которые подгружаются по мере необходимости.

Можно ли вызвать этот скилл вручную?

Нет. В нём стоит user-invocable: false и disable-model-invocation: true, так что он работает как справочник при написании кода, а не как команда.

Исходный текст

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

Смотрите также

Спонсоры проекта

Компании, которые помогают держать аналитику открытой