При построении валидации данных в реальных приложениях использование единой монолитной схемы быстро приводит к снижению поддерживаемости. Схемы начинают разрастаться, переплетаться между собой и терять читаемость. В таких условиях эффективным подходом становится разделение схем по доменам, при котором каждая предметная область приложения получает собственный набор независимых или слабо связанных валидационных описаний.
Библиотека Yup предоставляет гибкий механизм композиции схем, что делает её удобной основой для доменной архитектуры валидации.
Домен в контексте схем Yup — это логически изолированная область данных, отражающая конкретную бизнес-сущность:
Каждый домен содержит собственные правила валидации, которые не должны зависеть от внутренних деталей других доменов.
Такое разделение обеспечивает:
На уровне файловой организации каждая доменная схема выделяется в отдельный модуль.
Пример структуры:
schemas/
user.schema.js
auth.schema.js
profile.schema.js
billing.schema.js
Каждый файл экспортирует либо отдельные поля, либо целую схему.
Схема пользователя часто становится центральной в приложении, но при правильной декомпозиции она не превращается в монолит.
import * as yup from "yup";
export const userBaseSchema = yup.object({
id: yup.string().uuid().required(),
email: yup.string().email().required(),
createdAt: yup.date().required(),
});
Эта схема описывает только базовые свойства сущности, не включая бизнес-логику других доменов.
Yup позволяет расширять схемы с помощью concat, что
удобно для построения надстроек над базовыми сущностями.
import { userBaseSchema } from "./user.schema";
import * as yup from "yup";
export const userProfileSchema = userBaseSchema.concat(
yup.object({
firstName: yup.string().min(2).required(),
lastName: yup.string().min(2).required(),
avatarUrl: yup.string().url().nullable(),
})
);
Такой подход позволяет:
Домен аутентификации обычно не должен зависеть от бизнес-сущностей приложения.
import * as yup from "yup";
export const loginSchema = yup.object({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
});
export const registerSchema = yup.object({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
confirmPassword: yup
.string()
.oneOf([yup.ref("password")], "Пароли должны совпадать")
.required(),
});
Здесь домен изолирован и не использует внешние схемы, что снижает риск циклических зависимостей.
Одним из ключевых преимуществ Yup является возможность выделения переиспользуемых фрагментов.
export const emailField = yup.string().email().required();
export const passwordField = yup.string().min(8).required();
Далее они используются в разных доменах:
import { emailField, passwordField } from "../common/fields";
export const authSchema = yup.object({
email: emailField,
password: passwordField,
});
Такой подход превращает схемы в набор композиционных блоков.
При росте системы домены дополнительно делятся на уровни:
validation/
fields/
entities/
forms/
dto/
Каждый уровень использует предыдущий, но не наоборот.
lazy для сложных доменных зависимостейВ случаях, когда домены взаимно ссылаются друг на друга, применяется
lazy.
import * as yup from "yup";
export const commentSchema = yup.object({
id: yup.string().required(),
text: yup.string().required(),
author: yup.lazy(() => userBaseSchema),
});
lazy позволяет избежать циклического импорта и
откладывает вычисление схемы до момента выполнения.
Один и тот же домен может иметь разные представления в зависимости от контекста:
Пример для профиля:
export const profileCreateSchema = yup.object({
firstName: yup.string().required(),
lastName: yup.string().required(),
});
export const profileUpdateSchema = yup.object({
firstName: yup.string(),
lastName: yup.string(),
avatarUrl: yup.string().url(),
});
Такое разделение устраняет необходимость условной логики внутри одной схемы.
На уровне API часто требуется объединение нескольких доменов.
import { userBaseSchema } from "./user.schema";
import { profileSchema } from "./profile.schema";
export const userFullSchema = userBaseSchema.concat(profileSchema);
Однако важно избегать чрезмерного склеивания, которое снова приводит к монолиту.
Каждый домен должен содержать собственные ограничения, не зависящие от внешнего контекста.
export const billingSchema = yup.object({
cardNumber: yup
.string()
.matches(/^\d{16}$/, "Неверный номер карты")
.required(),
expiryDate: yup.date().required(),
});
Бизнес-логика платежей не должна проникать в домен пользователя или профиля.
Разделение по доменам позволяет тестировать каждую схему изолированно.
test("auth schema rejects short password", async () => {
await expect(
loginSchema.validate({
email: "test@mail.com",
password: "123",
})
).rejects.toThrow();
});
Чем меньше пересечений между доменами, тем проще поддерживать тестовую базу.
При отсутствии разделения схем возникают типичные проблемы:
Особенно критично это проявляется в формах с большим количеством полей из разных бизнес-областей.
Корректная архитектура предполагает однонаправленные зависимости:
fields → entities → forms → dto
Нарушение этого порядка приводит к:
При правильно выстроенной структуре изменение бизнес-правила затрагивает только один модуль.
Пример: изменение формата email влияет только на
fields/email, а не на десятки схем в разных частях
приложения.
При росте системы домены могут дробиться дальше:
Yup сохраняет гибкость за счёт возможности композировать схемы без изменения исходных блоков.
Для поддержания архитектурной чистоты важно фиксировать границы:
В зрелой архитектуре схема валидации перестаёт быть единым объектом и превращается в систему независимых модулей, взаимодействующих через композицию и переиспользование.
Yup в таком подходе выступает не просто библиотекой проверки данных, а инструментом построения структурированной модели данных на уровне приложения.