Клиентская валидация предназначена для проверки данных ещё до отправки формы на сервер. Такой подход позволяет:
Библиотека class-validator широко используется в экосистеме TypeScript и JavaScript для декларативной валидации объектов через декораторы. Несмотря на популярность на сервере, библиотека отлично подходит и для проверки клиентских форм.
Для работы библиотеки необходимы:
class-validatorclass-transformerreflect-metadataУстановка через npm:
npm install class-validator class-transformer reflect-metadata
Подключение reflect-metadata:
import 'reflect-metadata';
Для TypeScript необходимо включить декораторы в
tsconfig.json:
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
Библиотека использует классы как схемы валидации. Каждое свойство описывается набором декораторов.
Пример:
import { IsEmail, Length } from 'class-validator';
export class LoginForm {
@IsEmail()
email: string;
@Length(6, 20)
password: string;
}
Валидация выполняется функцией validate:
import { validate } from 'class-validator';
const form = new LoginForm();
form.email = 'wrong-email';
form.password = '123';
const errors = await validate(form);
console.log(errors);
Результатом проверки является массив объектов
ValidationError.
Пример структуры:
[
{
property: 'email',
value: 'wrong-email',
constraints: {
isEmail: 'email must be an email'
}
}
]
Основные поля:
| Поле | Описание |
|---|---|
property |
Имя поля |
value |
Полученное значение |
constraints |
Список ошибок |
children |
Вложенные ошибки |
<form id="register-form">
<input type="text" id="name">
<input type="email" id="email">
<button type="submit">Отправить</button>
</form>
Класс валидации:
import {
IsEmail,
Length
} from 'class-validator';
export class RegisterForm {
@Length(2, 30)
name: string;
@IsEmail()
email: string;
}
Обработка формы:
import { validate } from 'class-validator';
const formElement = document.getElementById('register-form');
formElement.addEventListener('submit', async (event) => {
event.preventDefault();
const form = new RegisterForm();
form.name = document.getElementById('name').value;
form.email = document.getElementById('email').value;
const errors = await validate(form);
if (errors.length > 0) {
console.log(errors);
return;
}
console.log('Форма корректна');
});
import {
IsString,
Length,
MinLength,
MaxLength,
Matches
} from 'class-validator';
Пример:
export class UserForm {
@IsString()
@Length(3, 20)
username: string;
@Matches(/^[a-zA-Z0-9]+$/)
login: string;
}
import { IsEmail } from 'class-validator';
export class EmailForm {
@IsEmail()
email: string;
}
import {
IsNumber,
Min,
Max
} from 'class-validator';
export class ProductForm {
@IsNumber()
@Min(1)
@Max(9999)
price: number;
}
import { IsBoolean } from 'class-validator';
export class SettingsForm {
@IsBoolean()
darkMode: boolean;
}
import {
IsDate,
MinDate
} from 'class-validator';
export class EventForm {
@IsDate()
@MinDate(new Date())
startDate: Date;
}
import { IsUrl } from 'class-validator';
export class WebsiteForm {
@IsUrl()
website: string;
}
Каждый декоратор поддерживает параметр message.
import { Length } from 'class-validator';
export class ProfileForm {
@Length(2, 10, {
message: 'Имя должно содержать от 2 до 10 символов'
})
name: string;
}
Сообщение может формироваться функцией.
import { MinLength } from 'class-validator';
export class PasswordForm {
@MinLength(8, {
message: (args) => {
return `Минимальная длина: ${args.constraints[0]}`;
}
})
password: string;
}
import {
IsNotEmpty,
IsDefined
} from 'class-validator';
export class ContactForm {
@IsDefined()
@IsNotEmpty()
message: string;
}
Разница:
| Декоратор | Поведение |
|---|---|
IsDefined |
Проверяет undefined и null |
IsNotEmpty |
Проверяет пустую строку |
import { Length } from 'class-validator';
export class Address {
@Length(2, 50)
city: string;
@Length(5, 100)
street: string;
}
import {
ValidateNested
} from 'class-validator';
import { Type } from 'class-transformer';
export class UserForm {
@ValidateNested()
@Type(() => Address)
address: Address;
}
import {
IsArray,
ArrayMinSize,
IsString
} from 'class-validator';
export class TagsForm {
@IsArray()
@ArrayMinSize(1)
@IsString({ each: true })
tags: string[];
}
Ключ { each: true } означает применение проверки к
каждому элементу массива.
import {
Matches,
MinLength
} from 'class-validator';
export class PasswordForm {
@MinLength(8)
@Matches(/[A-Z]/, {
message: 'Пароль должен содержать заглавную букву'
})
@Matches(/[0-9]/, {
message: 'Пароль должен содержать цифру'
})
password: string;
}
Декоратор ValidateIf позволяет включать проверку только
при выполнении условия.
import {
ValidateIf,
IsNotEmpty
} from 'class-validator';
export class PaymentForm {
paymentType: string;
@ValidateIf(o => o.paymentType === 'card')
@IsNotEmpty()
cardNumber: string;
}
Class-validator поддерживает асинхронные проверки.
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
Validate
} from 'class-validator';
Создание валидатора:
@ValidatorConstraint({ async: true })
export class IsEmailUniqueConstraint
implements ValidatorConstraintInterface {
async validate(email: string) {
const response = await fetch(`/api/check-email/${email}`);
const result = await response.json();
return result.isUnique;
}
defaultMessage(args: ValidationArguments) {
return 'Email уже используется';
}
}
Подключение:
export class RegisterForm {
@Validate(IsEmailUniqueConstraint)
email: string;
}
import {
registerDecorator,
ValidationOptions,
ValidationArguments
} from 'class-validator';
export function IsUsername(
validationOptions?: ValidationOptions
) {
return function (
object: Object,
propertyName: string
) {
registerDecorator({
name: 'isUsername',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: string) {
return /^[a-z0-9_]+$/i.test(value);
},
defaultMessage(args: ValidationArguments) {
return 'Недопустимый логин';
}
}
});
};
}
Использование:
export class UserForm {
@IsUsername()
username: string;
}
Данные формы часто приходят строками. Для преобразования используется
библиотека class-transformer.
import { plainToInstance } from 'class-transformer';
const formData = {
age: '25'
};
const instance = plainToInstance(UserForm, formData);
import { Type } from 'class-transformer';
export class UserForm {
@Type(() => Number)
age: number;
}
Синхронная версия валидации:
import { validateSync } from 'class-validator';
const errors = validateSync(form);
Подходит для:
input.addEventListener('input', async () => {
const form = new UserForm();
form.username = input.value;
const errors = await validate(form);
if (errors.length > 0) {
showError(errors[0]);
}
});
function getErrorMessage(error) {
return Object.values(error.constraints)[0];
}
function renderErrors(errors) {
errors.forEach(error => {
const field = document.querySelector(
`[name="${error.property}"]`
);
const message =
Object.values(error.constraints)[0];
field.classList.add('invalid');
const errorBlock =
document.createElement('div');
errorBlock.textContent = message;
field.after(errorBlock);
});
}
function clearErrors() {
document
.querySelectorAll('.error')
.forEach(el => el.remove());
document
.querySelectorAll('.invalid')
.forEach(el => {
el.classList.remove('invalid');
});
}
export class StepOne {
@IsEmail()
email: string;
}
export class StepTwo {
@MinLength(8)
password: string;
}
Каждый шаг проверяется отдельно:
const errors = await validate(currentStepData);
Группы позволяют применять разные правила.
export class UserForm {
@IsNotEmpty({
groups: ['create']
})
password: string;
}
Проверка:
await validate(form, {
groups: ['create']
});
validate(form, {
whitelist: true
});
Все свойства без декораторов будут удалены.
validate(form, {
forbidNonWhitelisted: true
});
Теперь наличие лишних полей вызовет ошибку.
Остановка после первой ошибки:
validate(form, {
stopAtFirstError: true
});
Полезно для крупных форм.
Причины:
reflect-metadata;tsconfig;HTML-формы всегда отправляют строки.
Решение:
@Type(() => Number)
IsDate() принимает только объект Date.
Неверно:
date = '2025-01-01'
Верно:
date = new Date('2025-01-01')
const [errors, setErrors] = useState([]);
Валидация:
async function submit() {
const form = plainToInstance(
RegisterForm,
values
);
const validationErrors =
await validate(form);
setErrors(validationErrors);
}
const form = reactive(
new RegisterForm()
);
const errors = ref([]);
Проверка:
errors.value = await validate(form);
Class-validator особенно популярен в проектах на TypeScript.
Пример:
const form = plainToInstance(
UserForm,
this.formGroup.value
);
const errors = await validate(form);
| Библиотека | Особенность |
|---|---|
| Class-validator | Декораторы и классы |
| Yup | Функциональные схемы |
| Zod | TypeScript-first подход |
| Joi | Мощная серверная валидация |
Правила располагаются прямо возле свойств.
@IsEmail()
email: string;
Один класс может использоваться:
Библиотека активно использует:
В проектах без TypeScript использование может быть неудобным.
Для небольших приложений библиотека может быть избыточной.
При глубокой структуре объектов код становится громоздким.
Оптимальная структура:
src/
├── forms/
├── validators/
├── models/
├── utils/
└── components/
import 'reflect-metadata';
import {
IsEmail,
MinLength,
Matches,
IsNotEmpty
} from 'class-validator';
export class RegisterForm {
@IsNotEmpty()
name: string;
@IsEmail()
email: string;
@MinLength(8)
@Matches(/[A-Z]/)
@Matches(/[0-9]/)
password: string;
}
Валидация:
import {
plainToInstance
} from 'class-transformer';
import {
validate
} from 'class-validator';
async function validateForm(data) {
const form = plainToInstance(
RegisterForm,
data
);
const errors = await validate(form, {
whitelist: true,
stopAtFirstError: true
});
return errors;
}
Удобный формат:
{
email: 'Некорректный email',
password: 'Пароль слишком короткий'
}
Преобразование:
function mapErrors(errors) {
return errors.reduce((acc, error) => {
acc[error.property] =
Object.values(error.constraints)[0];
return acc;
}, {});
}
import {
registerDecorator
} from 'class-validator';
export function IsFileSize(maxSize: number) {
return function (
object: Object,
propertyName: string
) {
registerDecorator({
name: 'isFileSize',
target: object.constructor,
propertyName,
validator: {
validate(file: File) {
return file.size <= maxSize;
}
}
});
};
}
Использование:
export class UploadForm {
@IsFileSize(1024 * 1024)
avatar: File;
}
export class ProfileForm {
@IsString()
@MinLength(2)
@MaxLength(20)
@Matches(/^[a-z]+$/i)
username: string;
}
Декораторы выполняются последовательно.
Метод выбрасывает исключение при ошибке.
import {
validateOrReject
} from 'class-validator';
try {
await validateOrReject(form);
} catch (errors) {
console.log(errors);
}
class BaseForm {
@IsEmail()
email: string;
}
class RegisterForm extends BaseForm {
@MinLength(8)
password: string;
}
Все декораторы наследуются автоматически.
При редактировании объекта часто требуется проверять только часть полей.
class UpdateUserForm {
@IsOptional()
@IsEmail()
email?: string;
}
import {
IsOptional,
IsString
} from 'class-validator';
export class UserForm {
@IsOptional()
@IsString()
middleName?: string;
}
Если поле отсутствует — остальные проверки пропускаются.
function StrongPassword() {
return applyDecorators(
MinLength(8),
Matches(/[A-Z]/),
Matches(/[0-9]/)
);
}
Использование:
class RegisterForm {
@StrongPassword()
password: string;
}
При больших формах полезно:
validateSync;stopAtFirstError;Клиентская проверка никогда не заменяет серверную.
Причины:
Клиентская валидация используется исключительно как дополнительный уровень проверки интерфейса.