Слои валидации в приложении

Валидация в приложениях редко должна существовать в одном единственном месте. На практике она распределяется по нескольким слоям, каждый из которых отвечает за собственный уровень проверки данных. Такой подход снижает связанность компонентов, упрощает сопровождение и позволяет точнее контролировать целостность информации на разных этапах её жизненного цикла.

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

Валидация в типичном приложении может быть разложена на несколько логических слоёв:

  • слой входных данных (API boundary)
  • слой транспортных объектов (DTO)
  • слой доменной модели
  • слой персистентности (база данных)
  • слой интеграций (внешние сервисы)

Каждый слой проверяет данные с разной степенью строгости и с разной целью. Ошибка в одном слое не всегда означает полную невалидность данных — иногда это лишь несоответствие контексту использования.

Слой входных данных

На границе системы данные считаются недоверенными по умолчанию. Основная задача — отсечь очевидно некорректные структуры до попадания в бизнес-логику.

С помощью Superstruct описывается строгая схема входящего запроса:

import { object, string, number, optional, validate } from "superstruct";

const CreateUserRequest = object({
  email: string(),
  password: string(),
  age: optional(number()),
});

function parseRequest(body) {
  const [error, value] = validate(body, CreateUserRequest);
  if (error) {
    throw new Error("Invalid request payload");
  }
  return value;
}

На этом уровне не проверяются бизнес-правила. Например, корректность email-формата или минимальная длина пароля могут проверяться позже или быть частью расширенной схемы.

Слой DTO (Data Transfer Objects)

DTO служат промежуточным представлением данных между внешним интерфейсом и доменной моделью. Здесь часто выполняется нормализация и частичная трансформация.

Пример расширения схемы:

import { refine, string } from "superstruct";

const Email = refine(string(), "Email", (value) =>
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
);

const UserDTO = object({
  email: Email,
  password: string(),
});

Валидация на уровне DTO уже ближе к бизнес-контексту, но всё ещё не должна содержать сложных зависимостей или обращений к внешним системам.

Слой доменной модели

Доменный слой отвечает за бизнес-правила. Здесь проверяется не только форма данных, но и их смысл в контексте предметной области.

Пример: пользователь не может быть младше определённого возраста, даже если DTO это пропустил.

function createUser(dto) {
  if (dto.age && dto.age < 18) {
    throw new Error("User must be adult");
  }

  return {
    email: dto.email,
    passwordHash: hash(dto.password),
    role: "user",
  };
}

Superstruct на этом уровне может использоваться для композиции доменных инвариантов:

import { object, number, optional, refine } from "superstruct";

const AdultAge = refine(optional(number()), "AdultAge", (age) => {
  if (age === undefined) return true;
  return age >= 18;
});

const DomainUser = object({
  email: string(),
  age: AdultAge,
});

Однако в доменной модели часто предпочтительнее явная логика, а не полностью декларативные схемы, чтобы сохранить читаемость бизнес-правил.

Слой персистентности

Перед сохранением в базу данных данные должны соответствовать требованиям схемы хранения. Этот слой защищает систему от неконсистентных записей, даже если предыдущие уровни дали сбой.

const DatabaseUser = object({
  id: string(),
  email: string(),
  passwordHash: string(),
  createdAt: string(),
});

Здесь важна строгость структуры: отсутствие nullable-значений, корректные типы и обязательные поля.

Слой интеграций

При взаимодействии с внешними сервисами данные снова проходят валидацию, поскольку внешние системы могут нарушать контракты.

const ExternalUserResponse = object({
  id: string(),
  email: string(),
  status: string(),
});

Даже если контракт задокументирован, фактические ответы могут отличаться, поэтому дополнительная проверка обязательна.

Композиция схем в Superstruct

Одним из ключевых преимуществ Superstruct является возможность композиции структур.

import { object, string, number, optional } from "superstruct";

const BaseUser = object({
  email: string(),
  password: string(),
});

const ExtendedUser = object({
  ...BaseUser.schema,
  age: optional(number()),
});

Более сложные сценарии используют intersect и union для объединения правил:

import { intersect, object, string, number } from "superstruct";

const HasEmail = object({
  email: string(),
});

const HasAge = object({
  age: number(),
});

const User = intersect([HasEmail, HasAge]);

Такая модель позволяет строить независимые валидаторы и переиспользовать их в разных частях системы.

Разделение ответственности между слоями

Чёткое распределение валидации снижает дублирование логики и уменьшает риск рассинхронизации правил.

  • входной слой: защита от мусорных данных
  • DTO: нормализация и форматирование
  • домен: бизнес-инварианты
  • база данных: структурная целостность
  • интеграции: контрактная устойчивость

Перенос всей логики в один слой приводит к разрастанию кода и усложнению тестирования. Разделённая модель делает поведение системы предсказуемым и локализованным.

Ошибки валидации и их агрегация

Superstruct предоставляет механизм получения структурированных ошибок, что позволяет собирать информацию о всех нарушениях сразу.

import { validate } from "superstruct";

const [error, value] = validate(data, UserDTO);

if (error) {
  console.log(error.failures());
}

Агрегация ошибок особенно важна на уровне API, где требуется возвращать полный список проблем, а не останавливать проверку на первой.

Переиспользование валидаторов между слоями

Одной из типичных архитектурных ошибок является дублирование схем. Вместо этого используется разделение на базовые и производные структуры:

const Email = string();
const Password = string();

const BaseUser = object({
  email: Email,
  password: Password,
});

Эти примитивы могут использоваться во всех слоях без изменения поведения.

Инварианты как независимые структуры

Инварианты часто выделяются отдельно и применяются через композицию:

const NonEmptyString = refine(string(), "NonEmptyString", (v) => v.length > 0);

Такие конструкции позволяют описывать повторяющиеся правила один раз и использовать их везде, где требуется гарантия корректности.

Валидация как часть архитектуры

Валидация перестаёт быть вспомогательной логикой и становится частью архитектурного каркаса приложения. Разделение на слои делает её предсказуемой, а использование декларативных схем упрощает сопровождение.

Superstruct в этом контексте выступает как средство формализации этих слоёв, позволяя описывать данные через композицию простых правил и применять их на разных уровнях системы без дублирования логики.