Урок 04

Валидация через Zod

Zod — библиотека для валидации с выводом типов.

Установка

npm install zod

Базовый пример

import { z } from 'zod';

const schema = z.object({
    title: z.string().min(3).max(200),
    body: z.string().min(10),
    published: z.boolean().optional().default(false),
});

export async function POST(request: Request) {
    const json = await request.json();
    const result = schema.safeParse(json);

    if (!result.success) {
        return Response.json(
            { errors: result.error.flatten().fieldErrors },
            { status: 422 }
        );
    }

    const post = await db.post.create({ data: result.data });
    return Response.json(post, { status: 201 });
}

safeParse не бросает — возвращает { success, data | error }.

Типы из схемы

type PostInput = z.infer<typeof schema>;

Одно определение — и валидация, и тип.

Типы правил

const schema = z.object({
    // строки
    title: z.string().min(3, 'Минимум 3').max(200, 'Максимум 200'),
    email: z.string().email('Некорректный email'),
    url: z.string().url('Некорректный URL'),
    uuid: z.string().uuid(),

    // числа
    age: z.number().int().min(18).max(120),
    price: z.number().positive(),

    // булевы
    published: z.boolean().default(false),

    // даты
    createdAt: z.date(),
    dateString: z.string().datetime(),

    // enum
    status: z.enum(['draft', 'published', 'archived']),

    // массивы
    tags: z.array(z.string()).min(1).max(10),

    // опциональные
    bio: z.string().optional(),
    phone: z.string().nullable(),

    // вложенные
    author: z.object({
        name: z.string(),
        email: z.string().email(),
    }),
});

Трансформации

const schema = z.object({
    published: z.string().transform((v) => v === 'true'),
    age: z.string().transform(Number),
    tags: z.string().transform((v) => v.split(',')),
});

Или через coerce:

const schema = z.object({
    page: z.coerce.number().int().min(1).default(1),
    published: z.coerce.boolean().default(false),
});

Refine

Кастомная проверка:

const schema = z.object({
    password: z.string().min(8),
    confirm: z.string(),
}).refine((data) => data.password === data.confirm, {
    message: 'Пароли не совпадают',
    path: ['confirm'],
});

superRefine для сложных:

const schema = z.object({
    start: z.date(),
    end: z.date(),
}).superRefine((data, ctx) => {
    if (data.end <= data.start) {
        ctx.addIssue({
            code: z.ZodIssueCode.custom,
            message: 'Конец должен быть после начала',
            path: ['end'],
        });
    }
});

Форматирование ошибок

const result = schema.safeParse(json);

if (!result.success) {
    const errors = result.error.flatten().fieldErrors;
    // { title: ['Минимум 3'], body: ['Минимум 10'] }

    return Response.json({ errors }, { status: 422 });
}

Другие форматы:

  • flatten() — { fieldErrors, formErrors }
  • format() — вложенная структура
  • issues — массив всех ошибок

Query-параметры

const QuerySchema = z.object({
    page: z.coerce.number().int().min(1).default(1),
    limit: z.coerce.number().int().min(1).max(100).default(20),
    search: z.string().optional(),
});

export async function GET(request: Request) {
    const { searchParams } = new URL(request.url);

    const result = QuerySchema.safeParse(Object.fromEntries(searchParams));

    if (!result.success) {
        return Response.json({ errors: result.error.flatten() }, { status: 422 });
    }

    const { page, limit, search } = result.data;
    // ...
}

Расширение схем

const BaseSchema = z.object({
    title: z.string().min(3),
});

const CreateSchema = BaseSchema.extend({
    body: z.string().min(10),
});

const UpdateSchema = BaseSchema.partial();

Pick и Omit

const UserSchema = z.object({
    name: z.string(),
    email: z.string().email(),
    password: z.string().min(8),
});

const PublicUserSchema = UserSchema.omit({ password: true });
const AuthSchema = UserSchema.pick({ email: true, password: true });

Итоги

  • safeParse не бросает
  • z.infer даёт тип
  • refine для кастомных проверок
  • coerce для приведения
  • flatten для ошибок
  • Расширение через extend, pick, omit