Серверная валидация форм

Серверная валидация форм является критическим слоем защиты данных, поступающих от клиента, независимо от наличия клиентской проверки. Она обеспечивает целостность, предсказуемость и безопасность входящих данных, снижая риск некорректных операций в бизнес-логике и взаимодействии с базой данных.

Подход на основе схем позволяет описывать структуру данных декларативно. В рамках этого подхода Zod представляет собой библиотеку, ориентированную на строгую типизацию и проверку данных в рантайме с синхронизацией типов TypeScript.

Схема описывает форму объекта, его поля, типы и дополнительные ограничения:

import { z } from "zod";

const userSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  age: z.number().int().positive(),
});

Схема становится единственным источником правды для структуры данных, поступающих в систему.

Базовые типы и ограничения

Zod предоставляет набор примитивных валидаторов:

  • строки: z.string()
  • числа: z.number()
  • булевы значения: z.boolean()
  • даты: z.date()
  • массивы: z.array()

Дополнительные ограничения накладываются цепочками методов:

const schema = z.object({
  username: z.string().min(3).max(20),
  score: z.number().min(0).max(100),
});

Каждое ограничение добавляет условие, проверяемое в рантайме.

Преобразование входных данных

Сервер часто получает данные в виде строк, даже если ожидаются числа или даты. Для устранения этого несоответствия используется преобразование:

const schema = z.object({
  age: z.coerce.number(),
  createdAt: z.coerce.date(),
});

Механизм coercion выполняет приведение типов до валидации, снижая количество ошибок парсинга.

Безопасное выполнение проверки

Метод parse выбрасывает исключение при ошибке, тогда как safeParse возвращает структурированный результат:

const result = userSchema.safeParse(data);

if (!result.success) {
  console.log(result.error);
}

Структура результата:

  • success: true — данные валидны
  • success: false — содержит объект ошибки ZodError

Структура ошибок

ZodError содержит детализированную информацию о каждом нарушении:

  • путь до поля
  • тип ошибки
  • ожидаемое значение
  • фактическое значение

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

const formatted = result.error.format();

или более плоский вид:

const flat = result.error.flatten();

Это позволяет удобно возвращать ошибки клиенту в виде JSON.

Вложенные структуры

Сложные формы часто содержат вложенные объекты и массивы:

const schema = z.object({
  user: z.object({
    name: z.string(),
    contacts: z.array(
      z.object({
        type: z.string(),
        value: z.string(),
      })
    ),
  }),
});

Валидация проходит рекурсивно, сохраняя структуру путей ошибок.

Частичные схемы и обновления данных

При обработке PATCH-запросов используется частичная валидация:

const updateSchema = userSchema.partial();

Также применяются операции выбора и исключения полей:

const publicUserSchema = userSchema.omit({
  password: true,
});

и

const loginSchema = userSchema.pick({
  email: true,
  password: true,
});

Значения по умолчанию

Схемы могут задавать дефолтные значения:

const schema = z.object({
  role: z.string().default("user"),
});

Если поле отсутствует, оно автоматически дополняется.

Преобразование результата

После валидации данные могут трансформироваться:

const schema = z.string().transform((val) => val.trim().toLowerCase());

Трансформация выполняется после успешной проверки.

Асинхронная валидация

Некоторые проверки требуют обращения к внешним ресурсам:

const schema = z.string().refine(async (email) => {
  return await isEmailAvailable(email);
}, {
  message: "Email уже используется",
});

Асинхронные схемы требуют использования parseAsync или safeParseAsync.

Интеграция с HTTP-серверами

Валидация входящих запросов часто выполняется на уровне middleware.

Express

app.post("/users", async (req, res) => {
  const result = userSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json(result.error.flatten());
  }

  // result.data содержит валидные данные
});

Fastify

fastify.post("/users", async (request, reply) => {
  const data = userSchema.parse(request.body);
  return data;
});

Fastify также поддерживает схемы на уровне маршрутов, но Zod используется как независимый слой валидации.

Next.js (API routes)

export default function handler(req, res) {
  const result = userSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json(result.error.format());
  }

  res.status(200).json(result.data);
}

Типизация TypeScript

Одним из ключевых преимуществ схемного подхода является автоматическое получение типов:

type User = z.infer<typeof userSchema>;

Это устраняет дублирование типов между runtime-валидацией и компиляцией TypeScript.

Композиция схем

Схемы могут комбинироваться:

const baseSchema = z.object({
  id: z.string(),
});

const extendedSchema = baseSchema.extend({
  name: z.string(),
});

или объединяться:

const merged = z.intersection(schemaA, schemaB);

Санитизация данных

Валидация часто дополняется очисткой входных значений:

const schema = z.object({
  comment: z.string().transform((v) => v.trim()),
});

Это снижает риск хранения неконсистентных данных.

Работа с null и optional

Различие между отсутствующим значением и null фиксируется явно:

const schema = z.object({
  middleName: z.string().optional(),
  nickname: z.string().nullable(),
});
  • optional — поле может отсутствовать
  • nullable — поле может быть null

Условные схемы

Сложные формы могут требовать зависимых правил:

const schema = z.object({
  type: z.enum(["admin", "user"]),
  permissions: z.array(z.string()).optional(),
}).refine((data) => {
  if (data.type === "admin") return true;
  return data.permissions !== undefined;
});

Масштабирование валидации

При росте проекта схемы выносятся в отдельные модули, формируя слой контрактов данных между клиентом и сервером. Такой слой снижает связность бизнес-логики и упрощает сопровождение API.

Стабильность структуры данных становится независимой от реализации контроллеров и сервисов, а проверка входящих данных перемещается в строго определённую точку обработки запроса.