Создание централизованного хранилища сообщений

Централизованное управление сообщениями валидации позволяет отделить бизнес-логику проверки данных от текстов ошибок, упростить поддержку проекта и обеспечить единый стиль ответов API. В контексте class-validator это особенно важно, поскольку библиотека активно использует строки сообщений в декораторах и кастомных валидаторах, и без архитектурного подхода они быстро начинают дублироваться и расползаться по коду.

Сообщения валидации в типичном приложении встречаются в нескольких местах:

  • стандартные декораторы (@IsString, @IsEmail, @MinLength)
  • пользовательские валидаторы (registerDecorator)
  • трансформеры ошибок (ValidationError → API response)
  • сервисы локализации (i18n)
  • бизнес-валидация на уровне DTO

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

Централизованное хранилище решает эту проблему за счёт выделения слоя абстракции над текстами ошибок.

Базовая структура хранилища сообщений

На практике чаще всего используется объект-конфиг, разделённый по доменам:

export const ValidationMessages = {
  user: {
    emailInvalid: 'Некорректный формат электронной почты',
    passwordTooShort: 'Пароль должен содержать минимум 8 символов',
    passwordTooWeak: 'Пароль не соответствует требованиям сложности',
    nameRequired: 'Имя обязательно для заполнения',
  },

  common: {
    required: 'Поле обязательно для заполнения',
    invalidString: 'Ожидается строковое значение',
    invalidNumber: 'Ожидается числовое значение',
  },

  auth: {
    tokenMissing: 'Токен отсутствует',
    tokenInvalid: 'Недействительный токен',
  }
};

Такое разделение позволяет:

  • группировать сообщения по предметным областям
  • избегать коллизий имен
  • упрощать масштабирование системы сообщений

Интеграция с декораторами class-validator

Большинство стандартных валидаторов поддерживают параметр 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

Более строгая архитектура предполагает отсутствие текстов в 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 || 'Ошибка валидации';
  }
}

Перехват и трансформация ValidationError

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-ответов

В крупных системах централизованное хранилище сообщений становится частью единого слоя формирования 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';

Это снижает вероятность ошибок при обращении к несуществующим сообщениям.

Итоговая структура слоя сообщений

Типичная зрелая архитектура включает:

  • статический словарь сообщений
  • фабрики параметризованных строк
  • резолвер ключей
  • i18n слой
  • трансформер ValidationError
  • сервис форматирования сообщений
  • единый API response mapper

Такая структура превращает сообщения валидации в управляемую подсистему, полностью отделённую от бизнес-логики и DTO, сохраняя консистентность поведения class-validator во всех частях приложения.