Валидация данных часто воспринимается как вспомогательный слой: проверка типов, обязательных полей и ограничений длины. В крупных системах схема становится центральным элементом архитектуры. Именно схема определяет:
В библиотеке Joi схема может выступать единым источником истины — объектом, вокруг которого строится работа приложения.
Типичная проблема backend-приложений — размножение одинаковых ограничений в разных местах.
Например:
// frontend
if (username.length < 3)
// controller
if (!req.body.username)
// service
if (user.username.length > 30)
// database
VARCHAR(30)
При изменении бизнес-логики приходится обновлять множество слоёв.
Появляются расхождения:
Схема Joi позволяет централизовать эти правила.
Схема становится описанием объекта:
const userSchema = Joi.object({
username: Joi.string()
.min(3)
.max(30)
.required(),
email: Joi.string()
.email()
.required(),
age: Joi.number()
.integer()
.min(18)
})
Теперь вся информация о структуре пользователя хранится в одном месте:
Схема может использоваться:
module.exports = {
userSchema
}
Далее:
const { userSchema } = require('./schemas/user')
await userSchema.validateAsync(payload)
Client
↓
HTTP API
↓
Joi schema
↓
Service layer
↓
Database
Сервисный слой получает уже валидированные данные.
Это означает:
Joi умеет не только проверять данные, но и преобразовывать их.
const schema = Joi.object({
age: Joi.number()
})
const result = schema.validate({
age: '25'
})
Результат:
{
value: {
age: 25
}
}
Строка автоматически преобразована в число.
const schema = Joi.object({
email: Joi.string()
.trim()
.lowercase()
})
const result = schema.validate({
email: ' ADMIN@SITE.COM '
})
Результат:
{
value: {
email: 'admin@site.com'
}
}
Схема становится не только валидатором, но и механизмом стандартизации данных.
Без схемы приложение начинает принимать множество форматов:
{
active: "true"
}
{
active: 1
}
{
active: "yes"
}
{
active: true
}
Схема устраняет неопределённость:
const schema = Joi.object({
active: Joi.boolean().required()
})
Joi-схема уже содержит описание модели.
Даже без внешней документации разработчик видит:
const productSchema = Joi.object({
title: Joi.string()
.max(200)
.required(),
price: Joi.number()
.positive()
.precision(2)
.required(),
category: Joi.string()
.valid(
'books',
'electronics',
'games'
),
tags: Joi.array()
.items(Joi.string())
.max(10)
})
Такой объект фактически является декларативным описанием сущности.
app.post('/users', async (req, res) => {
try {
const value = await userSchema.validateAsync(req.body)
const user = await userService.create(value)
res.json(user)
} catch (err) {
res.status(400).json({
error: err.message
})
}
})
await userCreatedSchema.validateAsync(event)
const envSchema = Joi.object({
PORT: Joi.number().required(),
DB_HOST: Joi.string().required(),
JWT_SECRET: Joi.string().required()
}).unknown()
const { error, value } = envSchema.validate(process.env)
if (error) {
throw error
}
Теперь приложение гарантированно стартует только при корректной конфигурации.
Валидация напрямую связана с безопасностью.
const schema = Joi.object({
username: Joi.string()
})
schema.validate(data, {
allowUnknown: false
})
Попытка передать:
{
username: 'admin',
role: 'superuser'
}
вызовет ошибку.
Это предотвращает:
const schema = Joi.object({
username: Joi.string()
})
const result = schema.validate(data, {
stripUnknown: true
})
Лишние поля автоматически удаляются.
Вход:
{
username: 'alex',
isAdmin: true
}
Результат:
{
username: 'alex'
}
Схемы помогают поддерживать разные версии API.
const userV1 = Joi.object({
username: Joi.string().required()
})
const userV2 = Joi.object({
username: Joi.string().required(),
email: Joi.string().email().required()
})
Маршруты используют нужную схему:
routerV1.post('/users', validate(userV1))
routerV2.post('/users', validate(userV2))
Большие системы редко используют монолитные схемы.
Joi поддерживает композицию.
const baseUser = Joi.object({
username: Joi.string().required()
})
const adminUser = baseUser.keys({
permissions: Joi.array()
.items(Joi.string())
})
const createUserSchema = Joi.object({
username: Joi.string().required(),
password: Joi.string().required()
})
const updateUserSchema = Joi.object({
username: Joi.string(),
password: Joi.string()
})
const publicUserSchema = Joi.object({
id: Joi.number(),
username: Joi.string()
})
Позволяет изменить часть схемы.
const schema = Joi.object({
username: Joi.string(),
email: Joi.string()
})
const requiredSchema = schema.fork(
['username', 'email'],
field => field.required()
)
Схема может содержать бизнес-правила.
const schema = Joi.object({
role: Joi.string()
.valid('user', 'admin'),
permissions: Joi.when('role', {
is: 'admin',
then: Joi.array()
.items(Joi.string())
.required(),
otherwise: Joi.forbidden()
})
})
const schema = Joi.object({
password: Joi.string(),
repeatPassword: Joi.string()
})
.with('password', 'repeatPassword')
const schema = Joi.object({
email: Joi.string(),
phone: Joi.string()
}).xor('email', 'phone')
Требуется только одно поле.
Даже тестирование может опираться на Joi-схемы.
expect(() => {
userSchema.validateAsync(fixture)
}).not.toThrow()
В экосистеме существуют инструменты:
Схема становится центральной моделью приложения.
Несмотря на динамическую природу Joi, схема может быть синхронизирована с типами.
type User = {
username: string
}
const schema = Joi.object({
username: Joi.string(),
age: Joi.number()
})
Типы и схема начинают расходиться.
Используются:
В schema-first архитектуре схема считается первичной сущностью.
Не типы определяют валидацию, а схема определяет:
Все ограничения находятся в одном месте.
Меньше расхождений между:
Изменение схемы автоматически влияет на всё приложение.
Схема уже описывает контракт данных.
src/
├── schemas/
│ ├── user/
│ │ ├── create.js
│ │ ├── update.js
│ │ └── public.js
│ │
│ ├── product/
│ └── auth/
│
├── middleware/
│ └── validate.js
│
├── services/
└── controllers/
const validate = schema => {
return async (req, res, next) => {
try {
req.validated = await schema.validateAsync(
req.body,
{
abortEarly: false,
stripUnknown: true
}
)
next()
} catch (err) {
res.status(400).json({
errors: err.details
})
}
}
}
Остановка на первой ошибке.
Сбор всех ошибок:
[
{
"message": "\"email\" is required"
},
{
"message": "\"password\" length must be at least 8 characters long"
}
]
Если сервис получает только валидированные данные, бизнес-логика становится значительно проще.
Вместо:
if (!user.email)
сервис может исходить из гарантии корректности структуры.
Joi отвечает за:
Сервис отвечает за:
Плохой подход:
if (!email)
if (typeof email !== 'string')
if (!email.includes('@'))
if (email.length > 255)
Правильный подход:
const schema = Joi.object({
email: Joi.string()
.email()
.max(255)
.required()
})
const schema = Joi.string().custom((value, helpers) => {
if (value.startsWith('admin')) {
return helpers.error('string.invalid')
}
return value
})
Joi поддерживает асинхронные проверки через external.
const schema = Joi.object({
email: Joi.string().email()
}).external(async value => {
const exists = await db.users.exists(value.email)
if (exists) {
throw new Error('Email already exists')
}
})
Схема — живая часть системы.
Она развивается вместе с бизнес-логикой:
При schema-first архитектуре изменения локализованы.
В зрелых проектах Joi-схема становится фундаментом:
Single Source of Truth означает:
структура данных определяется ровно в одном месте.
Для Joi этим местом является схема.
Все остальные части системы должны опираться именно на неё:
При правильной организации проекта: