Архитектура разделения схем валидации между клиентской и серверной частями приложения становится критически важной при росте сложности продукта. При использовании Superstruct единый источник правды для структур данных позволяет устранить дублирование логики, уменьшить количество ошибок несоответствия контрактов и упростить сопровождение.
В классической архитектуре фронтенд и бэкенд часто развиваются независимо. Это приводит к расхождению в ожиданиях относительно формата данных. Например, сервер может начать возвращать дополнительное поле, либо изменить тип существующего, а клиент при этом продолжит работать с устаревшей моделью.
Superstruct решает эту проблему через декларативное описание структур:
import { object, string, number, boolean } from 'superstruct'
export const User = object({
id: number(),
name: string(),
email: string(),
isActive: boolean(),
})
Такая структура становится контрактом, который может быть использован в обеих частях приложения без модификаций.
На практике схемы выносятся в отдельный пакет или модуль, который подключается как зависимость и в frontend, и в backend. Наиболее распространённая структура выглядит следующим образом:
/packages
/schemas
user.js
auth.js
product.js
/frontend
/backend
Слой schemas не содержит бизнес-логики, HTTP-клиентов
или UI-кода. Его задача — описывать только структуры данных.
Пример схемы в общем пакете:
// packages/schemas/user.js
import { object, string, number, optional } from 'superstruct'
export const CreateUser = object({
name: string(),
email: string(),
age: optional(number()),
})
На backend Superstruct используется для валидации входящих запросов. Это позволяет централизованно контролировать корректность данных до их попадания в бизнес-логику.
import { assert } from 'superstruct'
import { CreateUser } from '@app/schemas/user'
app.post('/users', (req, res) => {
try {
assert(req.body, CreateUser)
// бизнес-логика
const user = createUserInDatabase(req.body)
res.json(user)
} catch (e) {
res.status(400).json({ error: 'Invalid payload' })
}
})
Использование assert делает валидацию строгой: при
несоответствии структуры выбрасывается исключение, что позволяет
централизованно обрабатывать ошибки.
На frontend те же схемы применяются для проверки ответов API. Это снижает вероятность runtime-ошибок при изменении backend-контракта.
import { create } from 'superstruct'
import { User } from '@app/schemas/user'
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`)
const data = await res.json()
return create(data, User)
}
Функция create не только проверяет структуру, но и
возвращает типизированный объект, что упрощает дальнейшую работу с
данными.
При использовании TypeScript схемы Superstruct могут служить источником типов. Это устраняет необходимость ручного дублирования интерфейсов.
import { Infer } from 'superstruct'
import { User } from '@app/schemas/user'
export type UserType = Infer<typeof User>
Такой подход обеспечивает синхронность между runtime-валидацией и compile-time типизацией.
При развитии системы схемы неизбежно изменяются. В распределённой архитектуре важно избегать ломающих изменений.
Одним из решений является создание версионных схем:
export const UserV1 = object({
id: number(),
name: string(),
})
export const UserV2 = object({
id: number(),
name: string(),
email: string(),
})
Backend может поддерживать несколько версий одновременно, а клиент выбирать нужную в зависимости от контекста.
Superstruct поддерживает композицию, что позволяет строить сложные структуры из базовых блоков.
import { object, string, number, array } from 'superstruct'
const Address = object({
city: string(),
street: string(),
})
export const UserWithAddresses = object({
id: number(),
name: string(),
addresses: array(Address),
})
Такой подход уменьшает дублирование и упрощает сопровождение крупных моделей данных.
При использовании REST или GraphQL схемы могут применяться как промежуточный слой между транспортом и доменной логикой.
Для REST это обычно middleware-валидация. Для GraphQL — проверка входных аргументов резолверов.
import { assert } from 'superstruct'
import { LoginPayload } from '@app/schemas/auth'
const resolvers = {
Mutation: {
login: (_, args) => {
assert(args.input, LoginPayload)
return authService.login(args.input)
},
},
}
В системах с очередями сообщений (например, event-driven архитектура) схемы Superstruct используются для описания событий.
export const UserCreatedEvent = object({
type: string(),
payload: object({
userId: number(),
createdAt: string(),
}),
})
Такой подход позволяет гарантировать совместимость между сервисами, обменивающимися событиями.
При разделении схем между слоями важно обеспечить единый формат ошибок. Superstruct возвращает структурированные ошибки, которые можно нормализовать:
import { validate } from 'superstruct'
const [error, value] = validate(input, User)
if (error) {
console.log(error.failures())
}
Это позволяет формировать единый формат ответа API вне зависимости от точки возникновения ошибки.
В зрелых системах пакет схем становится инфраструктурным компонентом. Он версионируется отдельно и публикуется как внутренний npm-пакет.
Такой подход обеспечивает:
Для предотвращения рассинхронизации схем часто вводятся проверки в CI:
Это позволяет обнаруживать нарушения контрактов до попадания в production.
При всей эффективности разделения схем существуют ограничения. Жёсткая связка frontend и backend через общий пакет может усложнить независимое развёртывание сервисов. В распределённых системах иногда требуется дублирование схем с явной версификацией контрактов API.
Также Superstruct работает только на уровне runtime-валидации, не заменяя полноценные IDL-решения в сложных инфраструктурах.
Superstruct позволяет создавать собственные валидаторы, что особенно полезно при централизованном использовании схем:
import { define } from 'superstruct'
const PositiveNumber = define('PositiveNumber', (value) => {
return typeof value === 'number' && value > 0
})
Такие структуры также выносятся в общий пакет и используются синхронно на всех слоях системы.
При интеграции со сторонними сервисами общий слой схем позволяет нормализовать внешние данные:
const ExternalUser = object({
user_id: number(),
full_name: string(),
})
Далее данные приводятся к внутреннему формату через маппинг:
const normalizeUser = (data) => ({
id: data.user_id,
name: data.full_name,
})
Схемы при этом служат защитным слоем между внешними и внутренними контрактами.
Совместное использование Superstruct на frontend и backend формирует архитектуру, в которой:
Такой подход особенно эффективен в приложениях с интенсивным обменом данными и частыми изменениями API.