Domain-driven валидация

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

Библиотека Superstruct строит валидацию вокруг композиции простых структур. Каждая структура — это функция, которая принимает значение и либо возвращает его в нормализованном виде, либо выбрасывает ошибку.

Базовые примитивы:

import { string, number, boolean, array, object } from 'superstruct'

Каждый примитив уже содержит встроенную проверку:

string()   // только строки
number()   // только числа
boolean()  // только boolean

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

Композиция как основа доменной модели

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

Пример модели пользователя:

import { object, string, number } from 'superstruct'

const User = object({
  id: number(),
  name: string(),
  email: string()
})

На этом уровне структура уже отражает домен — пользователь как сущность системы, а не просто набор полей.

Однако реальная доменная модель редко ограничивается проверкой типов.

Ограничения и бизнес-правила

Доменные правила выражаются через модификаторы pattern, size, min, max, а также через кастомные проверки.

Пример ограничения email:

import { string, pattern } from 'superstruct'

const Email = pattern(
  string(),
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/
)

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

Расширение структуры через refine

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

import { refine, string } from 'superstruct'

const StrongPassword = refine(string(), 'StrongPassword', (value) => {
  return value.length >= 8 && /[A-Z]/.test(value) && /\d/.test(value)
})

Здесь уже проявляется доменная специфика: пароль — это не просто строка, а строка, удовлетворяющая политике безопасности системы.

Доменные сущности как композиция правил

В реальных приложениях сущности редко состоят из примитивов. Они комбинируются из других доменных структур.

import { object, string, number, refine } from 'superstruct'

const PositiveNumber = refine(number(), 'PositiveNumber', (v) => v > 0)

const Product = object({
  id: string(),
  price: PositiveNumber,
  title: string()
})

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

Нормализация данных

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

Пример нормализации строк:

import { coerce, string } from 'superstruct'

const TrimmedString = coerce(string(), string(), (value) => value.trim())

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

Работа с объектами как с агрегатами

В предметно-ориентированном подходе объект часто рассматривается как агрегат — целостная единица с внутренними правилами.

import { object, string, array, number, refine } from 'superstruct'

const OrderItem = object({
  productId: string(),
  quantity: refine(number(), 'Quantity', (v) => v > 0)
})

const Order = object({
  id: string(),
  items: array(OrderItem)
})

Здесь важно, что правила применяются не только к полям, но и к структуре в целом.

Валидация на уровне агрегата

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

import { refine, array } from 'superstruct'

const NonEmptyArray = (struct) =>
  refine(array(struct), 'NonEmptyArray', (value) => value.length > 0)

Такой подход отражает бизнес-правило на уровне агрегата, а не отдельного поля.

Ошибки как часть доменной модели

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

import { validate } from 'superstruct'

const [error, result] = validate(data, User)

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

Типизация и вывод типов

При использовании TypeScript Superstruct позволяет извлекать типы напрямую из структур.

import { Infer, object, string } from 'superstruct'

const User = object({
  id: string(),
  name: string()
})

type UserType = Infer<typeof User>

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

Переиспользование доменных правил

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

const UUID = pattern(string(), /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)

const UserId = UUID
const OrderId = UUID

Одна и та же структура используется в разных контекстах домена, сохраняя согласованность правил.

Декомпозиция сложных моделей

Сложные домены требуют разбиения на независимые структуры.

const Address = object({
  city: string(),
  street: string(),
  zip: string()
})

const UserProfile = object({
  name: string(),
  address: Address
})

Такой подход позволяет отражать иерархию предметной области напрямую в коде.

Инварианты и их выражение

Инварианты — это условия, которые всегда должны быть истинными. В Superstruct они выражаются через комбинацию refine и композиции структур.

const Age = refine(number(), 'Age', (v) => v >= 0 && v <= 120)

Инварианты становятся частью модели, а не внешней логики.

Динамическая сборка структур

В некоторых доменах структура зависит от условий.

const createUserStruct = (isAdmin) =>
  object({
    name: string(),
    role: isAdmin ? string() : pattern(string(), /^user$/)
  })

Это позволяет моделировать контекстно-зависимые правила.

Контроль границ домена

Валидация служит не только проверкой данных, но и границей между слоями системы. Superstruct формирует чёткий контракт входных данных, отделяя внешние источники от внутренней модели.

Любой внешний объект должен пройти через структуру, соответствующую доменной модели, прежде чем попасть в бизнес-логику.

Композиция как основной инструмент моделирования

Основная сила подхода заключается в том, что сложные правила строятся из простых элементов:

  • примитивы описывают базовые типы
  • refine добавляет бизнес-ограничения
  • object формирует сущности
  • array задаёт коллекции
  • композиция объединяет всё в агрегаты

Так формируется система, в которой валидация становится отражением предметной области, а не техническим слоем проверки данных.