1. Подходы к валидации: ручная проверка vs. библиотеки
Валидация (от англ. validation — "подтверждение правильности") — это процесс проверки входящих данных на соответствие определённым правилам: типам, диапазонам значений, обязательности полей и т.д.
Почему это важно:
- Безопасность: если не проверять, что клиент прислал правильные данные, можно получить ошибки, уязвимости, или даже "сломать" базу данных.
- Удобство: корректные ошибки для клиента, а не просто "500 Internal Server Error".
- Предсказуемость: ваш backend всегда работает с ожидаемым форматом данных.
- Меньше багов: меньше "магических" падений приложения из-за неожиданных undefined, null или "42" вместо email.
В Next.js Route Handlers валидация особенно актуальна, потому что:
- Вы часто принимаете данные в формате JSON (например, из форм или fetch-запросов).
- Ваш API может быть публичным (или почти публичным).
- Вы хотите возвращать дружелюбные и понятные ошибки.
Вариант 1: Ручная проверка (oldschool)
Можно проверять каждое поле "вручную", например:
// Пример Route Handler (POST /api/users)
export async function POST(req) {
const body = await req.json();
if (typeof body.name !== 'string' || body.name.length < 2) {
return new Response('Invalid name', { status: 400 });
}
if (typeof body.email !== 'string' || !body.email.includes('@')) {
return new Response('Invalid email', { status: 400 });
}
// ...
// Если всё ок — продолжаем обработку
}
Минусы:
- Много копипасты.
- Сложно поддерживать, если схема сложная.
- Легко ошибиться (например, забыть проверить какое-то поле).
- Никакой автоматической генерации ошибок.
Вариант 2: Использование библиотеки для валидации (best practice)
Современный подход — использовать специализированные библиотеки для декларативного описания схемы данных и автоматической проверки.
Плюсы:
- Лаконично и наглядно.
- Гибкая настройка (разные типы, вложенность, массивы, условия).
- Автоматическая генерация подробных ошибок.
- Возможность автогенерации типов TypeScript.
- Легко тестировать и переиспользовать схемы.
Самые популярные библиотеки:
- zod — фаворит современного фронтенда и Next.js.
- yup — раньше был стандартом де-факто, сейчас уступает zod.
- joi — очень мощная, популярна в Node.js.
- superstruct — легковесная, похожа на zod.
В этой лекции мы подробно разберём zod (он идеально вписывается в Next.js), а также кратко коснёмся других вариантов.
2. Основы работы с zod
Установка
Для начала, установим zod (в проекте Next.js):
npm install zod
# или
yarn add zod
Простейший пример схемы
import { z } from "zod";
// Описываем схему данных пользователя
const userSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().int().min(0).max(120).optional(),
});
- z.string() — строка.
- .min(2) — минимум 2 символа.
- .email() — валидный email.
- .number().int().min(0).max(120).optional() — необязательное целое число от 0 до 120.
Проверка данных по схеме
const data = {
name: "Иван",
email: "ivan@example.com",
age: 25,
};
const result = userSchema.safeParse(data);
if (!result.success) {
console.log(result.error.issues);
// Здесь массив ошибок, можно вернуть его клиенту
} else {
console.log(result.data); // Гарантированно валидные данные
}
- safeParse не выбрасывает исключение, а возвращает объект с success и error.
- Если данные валидны — они лежат в result.data.
- Если нет — в result.error.issues подробное описание ошибок.
3. Валидация данных в Route Handler: пошаговый пример
Шаг 1: Описываем схему
// /app/api/users/route.js
import { z } from "zod";
const userSchema = z.object({
name: z.string().min(2, "Имя должно быть не короче 2 символов"),
email: z.string().email("Неверный формат email"),
age: z.number().int().min(0, "Возраст не может быть отрицательным").max(120).optional(),
});
Шаг 2: Используем схему для валидации данных
export async function POST(req) {
let body;
try {
body = await req.json();
} catch {
return Response.json({ error: "Некорректный JSON" }, { status: 400 });
}
const result = userSchema.safeParse(body);
if (!result.success) {
// Собираем удобочитаемые сообщения об ошибках
const errors = result.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
}));
return Response.json({ errors }, { status: 400 });
}
// Данные валидны!
const user = result.data;
// Здесь можно сохранять пользователя, отправлять ответ и т.д.
return Response.json({ user }, { status: 201 });
}
Обратите внимание:
- Если JSON некорректный — сразу возвращаем ошибку.
- Если поля не проходят валидацию — возвращаем массив ошибок, чтобы фронтенд мог их красиво показать.
- Если всё ок — работаем с гарантированно валидными данными.
4. Валидация сложных структур: вложенные объекты, массивы
zod умеет валидировать не только "плоские" объекты, но и вложенные структуры, массивы, опциональные поля и т.д.
Массив пользователей
const usersSchema = z.array(userSchema);
const result = usersSchema.safeParse([
{ name: "Анна", email: "anna@example.com" },
{ name: "Боб", email: "bob@example.com", age: 200 }, // Ошибка: возраст
]);
Вложенные объекты
const addressSchema = z.object({
city: z.string(),
zip: z.string().length(6),
});
const userWithAddressSchema = userSchema.extend({
address: addressSchema.optional(),
});
5. Полезные нюансы
Генерация типов TypeScript из схемы
Одна из самых крутых фишек zod — генерация типов TS на лету:
import { z } from "zod";
const userSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().optional(),
});
// Тип User будет совпадать со схемой!
type User = z.infer<typeof userSchema>;
Теперь вы можете использовать тип User везде в коде, и если измените схему — тип обновится автоматически.
Сравнение zod, yup, joi, superstruct
| Библиотека | Синтаксис | TS-интеграция | Сообщества | Особенности |
|---|---|---|---|---|
| zod | Ясный, декларативный | Отличная | Очень активное | Прямо для TypeScript, быстрый |
| yup | Похож на joi | Посредственная | Среднее | Близок по стилю к joi, но проще |
| joi | Более громоздкий | Нет | Большое | "Классика" Node.js, для больших схем |
| superstruct | Минималистичный | Хорошая | Маленькое | Очень легкий, чуть менее гибкий |
Почему zod?
- Легко учить и читать.
- Прекрасно работает с TypeScript.
- Очень быстро развивается.
- Идеально подходит для Next.js и современных проектов.
Интеграция с Frontend: валидация на клиенте и сервере
zod можно использовать не только в Route Handlers, но и на клиенте — например, для проверки форм. Это позволяет использовать одну и ту же схему и на фронте, и на бэке (DRY-подход).
Пример:
- Вынесли схему в /lib/schemas/user.ts.
- Импортируем её и в Route Handler, и в компонент формы.
- Валидация будет одинаковой и предсказуемой.
6. Типичные ошибки при валидации данных в Route Handlers
Ошибка №1: Не проверяется тело запроса на валидный JSON.
Если клиент отправил битый JSON, await req.json() выбросит ошибку. Всегда оборачивайте в try/catch и возвращайте понятное сообщение.
Ошибка №2: Не возвращается подробная информация об ошибках.
Плохо: просто "400 Bad Request". Хорошо: массив ошибок с указанием поля и сообщения.
Ошибка №3: Слишком "жёсткая" схема.
Если вы не используете .optional() для необязательных полей, клиенту придётся всегда отправлять все поля — это неудобно.
Ошибка №4: Валидация только на фронте, но не на сервере.
Никогда не доверяйте данным с клиента! Даже если у вас суперкрутой React с Formik и yup — на сервере всё равно должна быть валидация.
Ошибка №5: Использование устаревших или плохо поддерживаемых библиотек.
Например, joi — отличная штука для старого Node.js, но для Next.js и TypeScript лучше выбрать zod.
Ошибка №6: Нарушение типов данных.
Если вы ожидаете число, а клиент прислал строку ("25" вместо 25), zod по умолчанию не преобразует типы (в отличие от yup). Используйте .transform() или .refine() для кастомных преобразований.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ