Централизованное управление сообщениями валидации позволяет отделить
бизнес-логику проверки данных от текстов ошибок, упростить поддержку
проекта и обеспечить единый стиль ответов API. В контексте
class-validator это особенно важно, поскольку библиотека
активно использует строки сообщений в декораторах и кастомных
валидаторах, и без архитектурного подхода они быстро начинают
дублироваться и расползаться по коду.
Сообщения валидации в типичном приложении встречаются в нескольких местах:
@IsString,
@IsEmail, @MinLength)registerDecorator)ValidationError → API
response)Без единого источника сообщений возникает фрагментация: одинаковые тексты дублируются, различаются формулировки, усложняется перевод и изменение формата ответа.
Централизованное хранилище решает эту проблему за счёт выделения слоя абстракции над текстами ошибок.
На практике чаще всего используется объект-конфиг, разделённый по доменам:
export const ValidationMessages = {
user: {
emailInvalid: 'Некорректный формат электронной почты',
passwordTooShort: 'Пароль должен содержать минимум 8 символов',
passwordTooWeak: 'Пароль не соответствует требованиям сложности',
nameRequired: 'Имя обязательно для заполнения',
},
common: {
required: 'Поле обязательно для заполнения',
invalidString: 'Ожидается строковое значение',
invalidNumber: 'Ожидается числовое значение',
},
auth: {
tokenMissing: 'Токен отсутствует',
tokenInvalid: 'Недействительный токен',
}
};
Такое разделение позволяет:
Большинство стандартных валидаторов поддерживают параметр
message, который может быть строкой или функцией.
import { IsEmail, MinLength } from 'class-validator';
import { ValidationMessages } from './validation-messages';
export class CreateUserDto {
@IsEmail({}, {
message: ValidationMessages.user.emailInvalid,
})
email;
@MinLength(8, {
message: ValidationMessages.user.passwordTooShort,
})
password;
}
Такой подход уже уменьшает дублирование, но остаётся проблема связности DTO с текстами.
Следующий уровень — создание функций-генераторов сообщений. Это позволяет отделить DTO от конкретных строк.
export const MessageFactory = {
required: (field) => `${field} обязательно для заполнения`,
minLength: (field, length) => `${field} должен содержать минимум ${length} символов`,
invalidFormat: (field) => `Некорректный формат поля ${field}`,
};
Использование:
import { IsString, MinLength } from 'class-validator';
import { MessageFactory } from './message-factory';
export class CreateUserDto {
@IsString({
message: MessageFactory.invalidFormat('Имя'),
})
name;
@MinLength(8, {
message: MessageFactory.minLength('Пароль', 8),
})
password;
}
Фабрика сообщений вводит параметризацию и уменьшает повторяемость строковых шаблонов.
Более строгая архитектура предполагает отсутствие текстов в DTO вообще. В этом случае используется маппинг правил валидации.
export const UserValidationRules = {
email: {
isEmail: true,
messageKey: 'user.emailInvalid',
},
password: {
minLength: 8,
messageKey: 'user.passwordTooShort',
},
};
DTO становится нейтральным:
import { IsEmail, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email;
@MinLength(8)
password;
}
Ключевым компонентом архитектуры становится слой преобразования
messageKey в текст.
import { ValidationMessages } from './validation-messages';
export class MessageResolver {
static resolve(key) {
const path = key.split('.');
let current = ValidationMessages;
for (const segment of path) {
current = current?.[segment];
}
return current || 'Ошибка валидации';
}
}
class-validator возвращает массив
ValidationError, содержащий вложенную структуру ошибок.
Именно на этом этапе централизованное хранилище проявляет максимальную
пользу.
import { MessageResolver } from './message-resolver';
export function mapValidationErrors(errors) {
const result = [];
const traverse = (errList) => {
for (const error of errList) {
if (error.constraints) {
const messages = Object.values(error.constraints).map((msg) => {
if (msg.startsWith('user.') || msg.startsWith('auth.')) {
return MessageResolver.resolve(msg);
}
return msg;
});
result.push({
field: error.property,
messages,
});
}
if (error.children && error.children.length) {
traverse(error.children);
}
}
};
traverse(errors);
return result;
}
Такой подход позволяет унифицировать формат ответа API независимо от того, где возникла ошибка.
При создании собственных валидаторов через
registerDecorator также применяется единый источник
сообщений.
import { registerDecorator } from 'class-validator';
import { ValidationMessages } from './validation-messages';
export function IsStrongPassword() {
return function (object, propertyName) {
registerDecorator({
name: 'IsStrongPassword',
target: object.constructor,
propertyName,
validator: {
validate(value) {
return /[A-Z]/.test(value) && /[0-9]/.test(value);
},
defaultMessage() {
return ValidationMessages.user.passwordTooWeak;
},
},
});
};
}
Централизация сообщений естественным образом расширяется до многоязычности.
export const MessagesI18n = {
ru: {
user: {
emailInvalid: 'Некорректный формат электронной почты',
},
},
en: {
user: {
emailInvalid: 'Invalid email format',
},
},
};
Резолвер учитывает язык:
export class I18nMessageResolver {
constructor(locale = 'ru') {
this.locale = locale;
}
resolve(key) {
const path = key.split('.');
let current = MessagesI18n[this.locale];
for (const segment of path) {
current = current?.[segment];
}
return current || key;
}
}
В крупных системах централизованное хранилище сообщений становится частью единого слоя формирования API-ответа.
export function createErrorResponse(errors) {
return {
status: 'error',
errors: errors.map((e) => ({
field: e.field,
messages: e.messages,
})),
};
}
Такой слой гарантирует стабильный контракт независимо от изменений в DTO или валидаторах.
В более сложной архитектуре вводится отдельный сервис сообщений:
export class ValidationMessageService {
constructor(resolver) {
this.resolver = resolver;
}
getMessage(key) {
return this.resolver.resolve(key);
}
formatMessage(key, params = {}) {
let message = this.getMessage(key);
for (const [param, value] of Object.entries(params)) {
message = message.replace(`{${param}}`, value);
}
return message;
}
}
При использовании TypeScript возможно создание строго типизированного набора ключей:
export type ValidationMessageKey =
| 'user.emailInvalid'
| 'user.passwordTooShort'
| 'auth.tokenMissing';
Это снижает вероятность ошибок при обращении к несуществующим сообщениям.
Типичная зрелая архитектура включает:
ValidationErrorТакая структура превращает сообщения валидации в управляемую
подсистему, полностью отделённую от бизнес-логики и DTO, сохраняя
консистентность поведения class-validator во всех частях
приложения.