Валидация в приложениях редко должна существовать в одном единственном месте. На практике она распределяется по нескольким слоям, каждый из которых отвечает за собственный уровень проверки данных. Такой подход снижает связанность компонентов, упрощает сопровождение и позволяет точнее контролировать целостность информации на разных этапах её жизненного цикла.
Superstruct часто используется как инструмент декларативного описания схем данных и построения многоуровневой валидации. Его ключевая ценность заключается в том, что структуры становятся композиционными: простые правила комбинируются в сложные модели без усложнения логики приложения.
Валидация в типичном приложении может быть разложена на несколько логических слоёв:
Каждый слой проверяет данные с разной степенью строгости и с разной целью. Ошибка в одном слое не всегда означает полную невалидность данных — иногда это лишь несоответствие контексту использования.
На границе системы данные считаются недоверенными по умолчанию. Основная задача — отсечь очевидно некорректные структуры до попадания в бизнес-логику.
С помощью 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 служат промежуточным представлением данных между внешним интерфейсом и доменной моделью. Здесь часто выполняется нормализация и частичная трансформация.
Пример расширения схемы:
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 является возможность композиции структур.
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]);
Такая модель позволяет строить независимые валидаторы и переиспользовать их в разных частях системы.
Чёткое распределение валидации снижает дублирование логики и уменьшает риск рассинхронизации правил.
Перенос всей логики в один слой приводит к разрастанию кода и усложнению тестирования. Разделённая модель делает поведение системы предсказуемым и локализованным.
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 в этом контексте выступает как средство формализации этих слоёв, позволяя описывать данные через композицию простых правил и применять их на разных уровнях системы без дублирования логики.