При росте приложения схемы валидации данных перестают быть локальными сущностями и превращаются в часть архитектуры. В Superstruct это особенно заметно, поскольку структуры данных легко комбинируются и переиспользуются. Без модульного подхода схемы быстро дублируются, становятся трудночитаемыми и начинают конфликтовать по логике.
Модульная организация строится вокруг нескольких базовых принципов:
В Superstruct каждая схема представляет собой функцию-валидатор, созданную из примитивов и композиционных операторов.
На уровне модульности важно выделить фундаментальные типы:
// structs/primitives.js
import { string, number, boolean } from 'superstruct'
export const ID = () => string()
export const Email = () => string()
export const Timestamp = () => number()
export const Flag = () => boolean()
Такие базовые элементы становятся общими зависимостями для всех остальных модулей. Их задача — обеспечить единообразие определения типов по всему приложению.
Наиболее устойчивый подход — группировка схем по предметным областям. Например, сущность пользователя изолируется в отдельном модуле.
// structs/user.js
import { object, string, optional } from 'superstruct'
import { ID, Email } from './primitives'
export const User = object({
id: ID(),
name: string(),
email: Email(),
nickname: optional(string())
})
Каждый модуль содержит только те структуры, которые относятся к конкретной предметной области. Это снижает связанность и упрощает тестирование.
В Superstruct отсутствует классическое наследование, поэтому расширение схем реализуется через композицию объектов.
import { object, string } from 'superstruct'
import { User } from './user'
export const Admin = object({
...User.schema,
role: string()
})
Такой подход позволяет создавать специализированные версии базовых структур без дублирования логики.
При усложнении домена композиция становится основным инструментом построения новых сущностей.
В модульной системе часто возникают повторяющиеся фрагменты схем: адреса, метаданные, настройки.
// structs/address.js
import { object, string, optional } from 'superstruct'
export const Address = object({
country: string(),
city: string(),
street: string(),
zip: optional(string())
})
Такие структуры используются в разных доменах без изменения:
import { object } from 'superstruct'
import { Address } from './address'
export const Company = object({
name: string(),
address: Address
})
export const Warehouse = object({
title: string(),
location: Address
})
Переиспользование снижает вероятность расхождения логики между модулями.
При большом количестве схем важно организовать единый слой экспорта, который упрощает импорт и предотвращает глубокие пути.
// structs/index.js
export * from './user'
export * from './admin'
export * from './address'
export * from './primitives'
Использование:
import { User, Admin } from './structs'
Это уменьшает связанность кода с файловой структурой и облегчает рефакторинг.
В зрелой архитектуре структуры данных разделяются по уровням:
Пример организации:
structs/
primitives.js
shared/
address.js
metadata.js
domain/
user.js
order.js
api/
userResponse.js
Такое разделение позволяет избежать смешивания внутренних и внешних форматов данных.
Модульный подход особенно эффективен при создании комплексных структур.
import { object, array } from 'superstruct'
import { User } from './user'
import { Address } from './address'
export const Order = object({
id: string(),
user: User,
shippingAddress: Address,
items: array(object({
productId: string(),
quantity: number()
}))
})
Каждая вложенная структура при этом остаётся независимым модулем, что позволяет изменять её без каскадных изменений.
В API часто требуется различие между входными и выходными структурами.
export const CreateUserInput = object({
name: string(),
email: string()
})
export const UserResponse = object({
id: string(),
name: string(),
email: string(),
createdAt: number()
})
Такая сегрегация предотвращает утечку внутренних полей в публичные интерфейсы.
При повторяющихся паттернах удобно применять функции-фабрики, создающие структуры динамически.
import { object, string } from 'superstruct'
export const createEntity = (extraFields) =>
object({
id: string(),
...extraFields
})
export const Product = createEntity({
title: string(),
description: string()
})
Фабрики позволяют стандартизировать базовую форму сущностей и уменьшить дублирование.
Модульная структура требует строгого контроля направлений зависимостей. Основное правило: нижние уровни не должны зависеть от верхних.
Нарушение проявляется при взаимных импортax:
user.js → order.js → user.js
Для устранения подобных ситуаций используются:
В некоторых случаях схема используется только внутри одного модуля. В таком случае её не следует выносить в глобальный слой.
const Item = object({
productId: string(),
quantity: number()
})
export const Cart = object({
items: array(Item)
})
Локальные структуры уменьшают засорение общей архитектуры и сохраняют контекстность.
Superstruct позволяет добавлять дополнительные правила поверх базовых
схем через refine.
import { string, refine } from 'superstruct'
export const PositiveString = refine(string(), (value) => {
return value.length > 0
})
Такие инварианты также удобно размещать в отдельных модулях, группируя их по типам проверок:
structs/constraints/
positiveString.js
emailFormat.js
В крупных проектах часто создаётся дополнительный слой абстракции:
object, string,
numberimport { object as sObject } from 'superstruct'
export const object = (schema) => sObject(schema)
Это позволяет менять поведение библиотеки без переписывания всех модулей.
Модульная структура напрямую влияет на тестирование. Изолированные схемы легко проверяются отдельно:
import { assert } from 'superstruct'
import { User } from './user'
assert({
id: '1',
name: 'Alex',
email: 'a@mail.com'
}, User)
Чем меньше связность между модулями, тем проще покрытие тестами и тем стабильнее поведение всей системы.
При увеличении количества сущностей модульная система сохраняет управляемость только при соблюдении нескольких условий:
Такая структура позволяет расширять систему без переработки существующих схем и без каскадных изменений по всему проекту