Input-типы и обработка ошибок мутаций
Input-типы группируют аргументы мутации в один объект, а паттерн payload с полем errors превращает ожидаемые ошибки в часть данных, а не в технический сбой.
Хорошая мутация принимает один аккуратный input и возвращает payload, где и результат, и понятные пользователю ошибки — это часть нормального ответа.
Когда у мутации много аргументов, перечислять их по одному неудобно и легко перепутать порядок. Input-тип — специальный тип для входных данных, объявляется ключевым словом input:
input CreateUserInput {
name: String!
email: String!
age: Int
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
Input-типы отличаются от обычных object-типов: их поля могут быть только скалярами, enum или другими input-типами — внутрь нельзя положить object-тип с резолверами. Это вход, а не результат. Логика тут простая: object-тип может содержать вычисляемые поля и связи, которые сервер резолвит на лету, — а входные данные клиент присылает целиком и сразу, резолвить там нечего. Поэтому GraphQL разводит эти две роли на разные виды типов. Ещё один плюс input-типа — переиспользование: один и тот же CreateUserInput можно принять и в мутации регистрации, и в импорте пользователей, не дублируя список полей. А раз input-поля типизированы (включая ! и enum), невалидные данные отсекаются ещё на валидации схемы, не доходя до твоей бизнес-логики.
Паттерн payload с ошибками
Технические сбои (упала БД) GraphQL кладёт в общий массив errors. Но ожидаемые ошибки бизнес-логики — «email занят», «слишком короткий пароль» — это часть нормального сценария. Их удобно возвращать прямо в данных, через тип-payload:
type UserError {
field: String
message: String!
}
type CreateUserPayload {
user: User
errors: [UserError!]!
}
Тогда клиент в одном ответе получает либо созданного user, либо список понятных errors с привязкой к полям формы:
{
"data": {
"createUser": {
"user": null,
"errors": [{ "field": "email", "message": "Email уже занят" }]
}
}
}
Как работает под капотом
Резолвер мутации получает input целиком, валидирует бизнес-правила и решает, что положить в payload: данные или ошибки. Смоделируем такой резолвер:
const users = [{ email: "[email protected]" }];
function createUser(_parent, { input }) {
const errors = [];
if (!input.name) errors.push({ field: "name", message: "Имя обязательно" });
if (!/.+@.+/.test(input.email))
errors.push({ field: "email", message: "Некорректный email" });
if (users.some(u => u.email === input.email))
errors.push({ field: "email", message: "Email уже занят" });
if (errors.length) return { user: null, errors };
const user = { id: users.length + 1, ...input };
users.push(user);
return { user, errors: [] }; // payload
}
console.log(createUser(null, { input: { name: "Аня", email: "[email protected]" } }));
console.log(createUser(null, { input: { name: "Боб", email: "[email protected]" } }));
Попробуй сам ▶ — передай пустое имя или занятый email и посмотри, как ошибки приезжают в errors payload, а не роняют запрос.
Частые ошибки
- Класть бизнес-ошибки в общий массив errors. «Email занят» — это нормальный ответ формы, а не сбой сервера. В общем
errorsтакие вещи неудобно привязывать к полям. - Гигантский список аргументов. Десять отдельных аргументов вместо одного input-типа — хрупко и нечитаемо.
- Возвращать только true/false. Клиент не узнает почему не получилось и не подсветит проблемное поле формы.
Best practices
- Для каждой мутации заводи свой input-тип (
CreateUserInput) и свой payload-тип (CreateUserPayload) — это масштабируемое соглашение. - Различай два класса ошибок: технические -> в
errorsверхнего уровня; ожидаемые бизнес-ошибки -> в полеerrorsвнутри payload с привязкой кfield. - Делай input-поля настолько строгими, насколько возможно (
String!, enum), чтобы невалидные данные отсекались ещё на валидации схемы.
Итоги
Input-типы группируют аргументы мутации в один объект и содержат только скаляры, enum и другие input-типы. Паттерн payload с полем errors возвращает ожидаемые бизнес-ошибки как часть данных, привязывая их к полям формы, тогда как технические сбои уходят в общий массив errors. Дальше — подписки и данные в реальном времени.