При разработке больших приложений схемы валидации быстро начинают повторяться. Одни и те же правила используются в формах регистрации, API-запросах, конфигурационных файлах, административных панелях и внутренних сервисах. Библиотека Joi предоставляет несколько механизмов для повторного использования схем без копирования кода.
Основные подходы:
.keys();.concat();Joi.ref);.shared() и .link();defaults;alter() и
tailor().Самый простой и распространённый способ — вынести повторяющиеся схемы в отдельные переменные.
const Joi = require('joi');
const emailSchema = Joi.string()
.email()
.required();
const passwordSchema = Joi.string()
.min(8)
.max(64)
.required();
Теперь схемы можно использовать повторно:
const registerSchema = Joi.object({
email: emailSchema,
password: passwordSchema
});
const loginSchema = Joi.object({
email: emailSchema,
password: passwordSchema
});
Такой подход:
.keys()Метод .keys() позволяет расширять существующие объектные
схемы.
const baseUserSchema = Joi.object({
id: Joi.number().integer().positive(),
email: Joi.string().email().required()
});
const fullUserSchema = baseUserSchema.keys({
firstName: Joi.string().required(),
lastName: Joi.string().required()
});
Полученная схема содержит:
{
id,
email,
firstName,
lastName
}
Часто часть структуры повторяется в нескольких схемах.
const addressSchema = Joi.object({
country: Joi.string().required(),
city: Joi.string().required(),
street: Joi.string().required(),
zipCode: Joi.string().required()
});
const userSchema = Joi.object({
name: Joi.string().required(),
address: addressSchema
});
const companySchema = Joi.object({
title: Joi.string().required(),
office: addressSchema
});
.concat()Метод .concat() объединяет две совместимые схемы.
const baseNameSchema = Joi.string();
const requiredNameSchema = baseNameSchema.concat(
Joi.string().min(3).required()
);
Результат:
Joi.string().min(3).required()
const timestampsSchema = Joi.object({
createdAt: Joi.date(),
updatedAt: Joi.date()
});
const postSchema = Joi.object({
title: Joi.string().required(),
content: Joi.string().required()
}).concat(timestampsSchema);
Во многих случаях правила зависят от параметров. Для этого удобно использовать функции.
function createNameSchema(min, max) {
return Joi.string().min(min).max(max).required();
}
Использование:
const shortName = createNameSchema(2, 20);
const longName = createNameSchema(10, 100);
Фабрики особенно полезны при создании CRUD-схем.
function createPaginationSchema(maxLimit = 100) {
return Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number()
.integer()
.min(1)
.max(maxLimit)
.default(10)
});
}
Часто набор правил применяется к разным полям.
const idSchema = Joi.number()
.integer()
.positive()
.required();
Использование:
const articleSchema = Joi.object({
articleId: idSchema,
authorId: idSchema
});
Схемы могут включать другие схемы любого уровня вложенности.
const geoSchema = Joi.object({
lat: Joi.number().required(),
lng: Joi.number().required()
});
const addressSchema = Joi.object({
city: Joi.string().required(),
coordinates: geoSchema
});
const storeSchema = Joi.object({
title: Joi.string().required(),
address: addressSchema
});
Joi.ref()Ссылки позволяют использовать значения других полей.
const schema = Joi.object({
password: Joi.string().required(),
confirmPassword: Joi.any()
.valid(Joi.ref('password'))
.required()
});
const schema = Joi.object({
user: Joi.object({
role: Joi.string().required()
}),
permissions: Joi.array().when('user.role', {
is: 'admin',
then: Joi.required(),
otherwise: Joi.optional()
})
});
.shared()Метод .shared() регистрирует схему для дальнейшего
использования через ссылки.
const schema = Joi.object({
user: Joi.link('#user')
}).shared(
Joi.object({
id: Joi.number().required(),
name: Joi.string().required()
}).id('user')
);
.link().link() позволяет ссылаться на ранее определённую
схему.
const schema = Joi.object({
author: Joi.object({
id: Joi.number().required(),
name: Joi.string().required()
}).id('person'),
reviewer: Joi.link('#person')
});
Это особенно полезно:
Одно из важнейших применений .link() — создание
рекурсивных структур.
const categorySchema = Joi.object({
title: Joi.string().required(),
children: Joi.array().items(
Joi.link('#category')
)
}).id('category');
Пример валидных данных:
{
title: 'Electronics',
children: [
{
title: 'Phones',
children: []
}
]
}
const idsSchema = Joi.array()
.items(
Joi.number().integer().positive()
)
.min(1);
Использование:
const deleteSchema = Joi.object({
ids: idsSchema.required()
});
const exportSchema = Joi.object({
selectedIds: idsSchema
});
В крупных проектах удобно создавать наборы базовых правил.
const schemas = {
id: Joi.number().integer().positive(),
email: Joi.string().email(),
slug: Joi.string().pattern(/^[a-z0-9-]+$/),
timestamp: Joi.date()
};
Использование:
const postSchema = Joi.object({
id: schemas.id.required(),
slug: schemas.slug.required(),
createdAt: schemas.timestamp
});
defaultsdefaults() позволяет автоматически применять правила ко
всем схемам определённого типа.
const customJoi = Joi.defaults(schema => {
switch (schema.type) {
case 'string':
return schema.trim();
case 'object':
return schema.options({
stripUnknown: true
});
default:
return schema;
}
});
Теперь:
const schema = customJoi.object({
name: customJoi.string()
});
Строки автоматически обрезаются через .trim().
В реальных проектах схемы обычно разделяются по файлам.
schemas/
├── common/
│ ├── id.schema.js
│ ├── email.schema.js
│ └── pagination.schema.js
│
├── user/
│ ├── create-user.schema.js
│ ├── update-user.schema.js
│ └── user.schema.js
│
└── post/
├── create-post.schema.js
└── update-post.schema.js
При обновлении сущности часть полей становится необязательной.
const createUserSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
age: Joi.number().required()
});
const updateUserSchema = createUserSchema.fork(
['name', 'email', 'age'],
schema => schema.optional()
);
.fork().fork() массово изменяет поля схемы.
const schema = Joi.object({
name: Joi.string(),
email: Joi.string().email(),
phone: Joi.string()
});
const requiredSchema = schema.fork(
['name', 'email'],
field => field.required()
);
.alter() и .tailor()Эти методы позволяют создавать адаптируемые схемы.
const schema = Joi.object({
title: Joi.string().alter({
create: schema => schema.required(),
update: schema => schema.optional()
}),
content: Joi.string().alter({
create: schema => schema.required(),
update: schema => schema.optional()
})
});
const createSchema = schema.tailor('create');
const updateSchema = schema.tailor('update');
const priceSchema = Joi.number().when('type', {
is: 'premium',
then: Joi.number().min(100),
otherwise: Joi.number().min(10)
});
Использование:
const productSchema = Joi.object({
type: Joi.string().required(),
price: priceSchema
});
const baseSchema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required()
});
const adminSchema = baseSchema.keys({
permissions: Joi.array()
.items(Joi.string())
.required()
});
const userSchema = baseSchema.keys({
subscription: Joi.string()
});
.append().append() добавляет поля к объектной схеме.
const baseSchema = Joi.object({
name: Joi.string()
});
const extendedSchema = baseSchema.append({
age: Joi.number()
});
На практике обычно используется комбинация нескольких подходов.
const idSchema = Joi.number()
.integer()
.positive();
const timestampsSchema = Joi.object({
createdAt: Joi.date(),
updatedAt: Joi.date()
});
function createEntitySchema(fields) {
return Joi.object(fields)
.keys({
id: idSchema
})
.concat(timestampsSchema);
}
Использование:
const articleSchema = createEntitySchema({
title: Joi.string().required(),
content: Joi.string().required()
});
const strongPassword = Joi.string().custom((value, helpers) => {
const hasUppercase = /[A-Z]/.test(value);
const hasNumber = /\d/.test(value);
if (!hasUppercase || !hasNumber) {
return helpers.error('password.weak');
}
return value;
});
Использование:
const schema = Joi.object({
password: strongPassword.required()
});
Схемы Joi иммутабельны.
const base = Joi.string();
const required = base.required();
console.log(base === required);
Результат:
false
Каждый вызов создаёт новую схему.
Это позволяет безопасно переиспользовать схемы без риска случайного изменения.
const emailSchema = Joi.string()
.email()
.lowercase()
.trim();
const createUserSchema = Joi.object({
email: emailSchema.required()
});
const updateUserSchema = Joi.object({
email: emailSchema.optional()
});
const createUserSchema = Joi.object({
email: Joi.string()
.email()
.lowercase()
.trim()
.required()
});
const updateUserSchema = Joi.object({
email: Joi.string()
.email()
.lowercase()
.trim()
});
Изменение правил потребует обновления сразу в нескольких местах.
Чрезмерное переиспользование тоже может создавать проблемы.
const base = Joi.object({...});
const level1 = base.keys({...});
const level2 = level1.keys({...});
const level3 = level2.keys({...});
Слишком длинные цепочки наследования:
Оптимальная схема переиспользования обычно включает:
Такой подход обеспечивает: