GraphQL-запросы строятся вокруг строгой схемы типов, однако типизация GraphQL не заменяет полноценную валидацию входных данных на уровне бизнес-логики. В сложных приложениях входные аргументы часто включают вложенные структуры, фильтры, сортировки, диапазоны дат и динамические параметры, которые требуют дополнительной проверки. Библиотека Joi предоставляет декларативный способ описания правил валидации и становится удобным инструментом для обработки input-слоёв GraphQL.
GraphQL гарантирует соответствие типов на уровне схемы, но не решает задачи:
Joi применяется в резолверах как дополнительный слой, который отделяет транспортную схему GraphQL от логики домена.
Типичная архитектура включает:
GraphQL input-объекты обычно соответствуют объектам JavaScript, которые приходят в аргументах резолвера.
Пример входного типа:
input CreateUserInput {
email: String!
password: String!
age: Int
}
Эквивалентная схема Joi:
import Joi fr om 'joi';
const createUserSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).max(64).required(),
age: Joi.number().integer().min(0).max(120).optional()
});
В резолвере:
const resolvers = {
Mutation: {
createUser: async (_, args) => {
const { error, value } = createUserSchema.validate(args.input);
if (error) {
throw new Error(error.details[0].message);
}
return userService.create(value);
}
}
};
GraphQL активно использует вложенные input-типы, особенно в фильтрации и сложных мутациях.
Пример:
input AddressInput {
city: String!
zip: String!
}
input UserProfileInput {
name: String!
address: AddressInput
}
Joi позволяет зеркально описывать структуру:
const addressSchema = Joi.object({
city: Joi.string().required(),
zip: Joi.string().pattern(/^\d{5}$/).required()
});
const userProfileSchema = Joi.object({
name: Joi.string().min(2).required(),
address: addressSchema.required()
});
Особенность вложенных схем заключается в возможности переиспользования и композиции, что особенно важно при росте количества input-типа в GraphQL API.
GraphQL часто передаёт списки идентификаторов или сложные фильтры.
Пример:
input UsersFilter {
ids: [ID!]
roles: [String!]
}
Joi-эквивалент:
const usersFilterSchema = Joi.object({
ids: Joi.array().items(Joi.string().uuid()).min(1).optional(),
roles: Joi.array().items(Joi.string().valid('admin', 'user', 'moderator'))
});
Механизм .items() позволяет точно контролировать
структуру каждого элемента массива, включая вложенные объекты.
В GraphQL часто встречаются взаимозависимые параметры, например:
email, либо phone;startDate не может быть больше
endDate;Joi поддерживает условную логику через when:
const schema = Joi.object({
email: Joi.string().email(),
phone: Joi.string().pattern(/^\+?\d+$/),
contactType: Joi.string().valid('email', 'phone').required()
}).or('email', 'phone');
Более сложные зависимости:
const periodSchema = Joi.object({
startDate: Joi.date().required(),
endDate: Joi.date().greater(Joi.ref('startDate')).required()
});
Такая модель позволяет переносить бизнес-ограничения из резолверов в декларативную схему.
GraphQL-клиенты могут отправлять данные в различных форматах. Joi способен не только валидировать, но и нормализовать вход:
const schema = Joi.object({
email: Joi.string().email().lowercase().trim(),
tags: Joi.array().items(Joi.string().trim().lowercase())
});
Нормализация снижает нагрузку на бизнес-логику и предотвращает дублирование обработки данных в разных частях системы.
При масштабировании проекта валидацию часто выносят из резолверов в отдельный слой.
Пример middleware-подхода:
const validate = (schema) => (resolver) => {
return async (parent, args, context, info) => {
const { error, value } = schema.validate(args.input);
if (error) {
throw new Error(error.details.map(d => d.message).join(', '));
}
return resolver(parent, { ...args, input: value }, context, info);
};
};
Использование:
const resolvers = {
Mutation: {
createUser: validate(createUserSchema)(async (_, { input }) => {
return userService.create(input);
})
}
};
Такой подход снижает связность кода и стандартизирует обработку ошибок.
GraphQL ожидает структурированные ошибки, и Joi предоставляет детализированную информацию:
error.details.forEach(d => ({
message: d.message,
path: d.path
}));
Эти данные могут быть преобразованы в GraphQL error extensions:
throw new GraphQLError('Validation error', {
extensions: {
code: 'BAD_USER_INPUT',
details: error.details
}
});
Это позволяет клиенту точно определить проблемное поле запроса.
В крупных системах схемы Joi становятся частью доменного слоя:
Композиция:
const basePagination = Joi.object({
lim it: Joi.number().min(1).max(100).default(20),
offset: Joi.number().min(0).default(0)
});
const productListSchema = basePagination.keys({
category: Joi.string().optional()
});
Переиспользование уменьшает дублирование и облегчает сопровождение API.
GraphQL подвержен рискам:
Joi помогает ограничить поверхность атаки:
const secureSchema = Joi.object({
query: Joi.string().max(1000).required(),
depth: Joi.number().max(5)
});
Ограничения глубины и размера входных данных предотвращают перегрузку сервера.
GraphQL часто использует гибкие фильтры:
input ProductFilter {
priceMin: Float
priceMax: Float
inStock: Boolean
search: String
}
Joi:
const productFilterSchema = Joi.object({
priceMin: Joi.number().min(0),
priceMax: Joi.number().min(Joi.ref('priceMin')),
inStock: Joi.boolean(),
search: Joi.string().max(200)
});
Подобные схемы формируют основу для построения динамических query-builder’ов.
Мутации в GraphQL часто включают несколько уровней вложенности:
input OrderItemInput {
productId: ID!
quantity: Int!
}
input CreateOrderInput {
userId: ID!
items: [OrderItemInput!]!
}
Joi:
const orderItemSchema = Joi.object({
productId: Joi.string().required(),
quantity: Joi.number().integer().min(1).required()
});
const createOrderSchema = Joi.object({
userId: Joi.string().required(),
items: Joi.array().items(orderItemSchema).min(1).required()
});
Такая структура обеспечивает строгую проверку целостности данных до выполнения транзакционной логики.
При росте API становится важным разделение ответственности:
Joi в этом контексте выполняет роль формального контракта между API и доменом, обеспечивая предсказуемость входных данных и снижая количество дефектов, связанных с некорректными аргументами запросов.