JS Tower
Node.jsМодуль 5: Базы данных

Валидация данных

Joi, Zod, express-validator

Цель урока

В этом уроке ты научишься:

  • Валидировать данные с Joi
  • Использовать Zod для TypeScript
  • Применять express-validator

Joi

Установка

npm install joi

Базовое использование

const Joi = require('joi');

const userSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(6).required(),
  age: Joi.number().integer().min(18).max(120),
  role: Joi.string().valid('user', 'admin').default('user')
});

// Валидация
const { error, value } = userSchema.validate(req.body);

if (error) {
  return res.status(400).json({ error: error.details[0].message });
}

Middleware

function validate(schema) {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, {
      abortEarly: false,  // Все ошибки
      stripUnknown: true  // Удалить неизвестные поля
    });
    
    if (error) {
      const errors = error.details.map(d => ({
        field: d.path.join('.'),
        message: d.message
      }));
      return res.status(400).json({ errors });
    }
    
    req.body = value;
    next();
  };
}

// Использование
app.post('/users', validate(userSchema), (req, res) => {
  // req.body уже валидирован
});

Сложные схемы

const postSchema = Joi.object({
  title: Joi.string().min(5).max(200).required(),
  content: Joi.string().min(10).required(),
  tags: Joi.array().items(Joi.string()).min(1).max(5),
  publishAt: Joi.date().greater('now'),
  author: Joi.object({
    id: Joi.number().required(),
    name: Joi.string()
  }).required()
});

// Условная валидация
const schema = Joi.object({
  type: Joi.string().valid('personal', 'business').required(),
  companyName: Joi.when('type', {
    is: 'business',
    then: Joi.string().required(),
    otherwise: Joi.forbidden()
  })
});

Zod (TypeScript)

Установка

npm install zod

Базовое использование

import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  password: z.string().min(6),
  age: z.number().int().min(18).max(120).optional(),
  role: z.enum(['user', 'admin']).default('user')
});

// Тип выводится автоматически
type User = z.infer<typeof userSchema>;

// Валидация
const result = userSchema.safeParse(req.body);

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

const user: User = result.data;

Middleware

import { z, ZodSchema } from 'zod';
import { Request, Response, NextFunction } from 'express';

function validate<T>(schema: ZodSchema<T>) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    
    if (!result.success) {
      return res.status(400).json({ 
        errors: result.error.errors.map(e => ({
          field: e.path.join('.'),
          message: e.message
        }))
      });
    }
    
    req.body = result.data;
    next();
  };
}

Продвинутые возможности

// Трансформация
const schema = z.string().transform(val => val.toLowerCase());

// Уточнение
const passwordSchema = z.string()
  .min(8)
  .refine(val => /[A-Z]/.test(val), 'Нужна заглавная буква')
  .refine(val => /[0-9]/.test(val), 'Нужна цифра');

// Объединение схем
const baseSchema = z.object({ id: z.number() });
const extendedSchema = baseSchema.extend({ name: z.string() });

// Частичная схема
const partialSchema = userSchema.partial();  // Все поля optional

express-validator

Установка

npm install express-validator

Использование

const { body, param, query, validationResult } = require('express-validator');

app.post('/users',
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 6 }).trim(),
  body('name').notEmpty().escape(),
  body('age').optional().isInt({ min: 18 }),
  (req, res) => {
    const errors = validationResult(req);
    
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    // Данные валидны
    res.json(req.body);
  }
);

Кастомные валидаторы

const { body } = require('express-validator');

body('email').custom(async (email) => {
  const user = await User.findOne({ email });
  if (user) {
    throw new Error('Email уже используется');
  }
});

body('confirmPassword').custom((value, { req }) => {
  if (value !== req.body.password) {
    throw new Error('Пароли не совпадают');
  }
  return true;
});

Сравнение

КритерийJoiZodexpress-validator
TypeScriptЧастичноОтличноЧастично
РазмерБольшойСреднийМаленький
СинтаксисFluentFluentMiddleware
ТрансформацияДаДаДа

Практика

Задание: Схема регистрации

Задача: Создай схему для регистрации с подтверждением пароля.

Решение (Joi):

const registerSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),
  confirmPassword: Joi.string()
    .valid(Joi.ref('password'))
    .required()
    .messages({ 'any.only': 'Пароли не совпадают' }),
  name: Joi.string().min(2).max(50).required()
});

Проверь себя

  1. Как валидировать email в Joi?
  2. Что делает safeParse в Zod?
  3. Как создать кастомный валидатор?