Что такое резолвер: четыре аргумента

Резолвер — это функция, которая знает, где взять данные для одного конкретного поля. У неё четыре аргумента: parent, args, context и info.

Схема говорит «что можно спросить», резолверы говорят «где это взять». Без резолверов схема — красивый, но мёртвый контракт.

Схема описывает форму данных, но сама по себе ничего не возвращает. За каждое поле отвечает резолвер — обычная функция. Сервер обходит дерево запроса и для каждого поля вызывает его резолвер. Сигнатура одинакова для всех полей и принимает до четырёх позиционных аргументов:

resolver(parent, args, context, info)
АргументЧто это
parentРезультат резолвера родительского поля (для вложенных полей)
argsАргументы, переданные этому полю в запросе
contextОбщий объект на весь запрос: текущий пользователь, БД, загрузчики
infoМетаданные о самом запросе (путь, какие поля запрошены)

Карта резолверов

Резолверы группируют по типам, повторяя структуру схемы. Серверный код (например на Apollo) выглядит так:

const resolvers = {
  Query: {
    user: (parent, args, ctx) => ctx.db.findUser(args.id),
  },
  User: {
    // parent здесь — это объект user из резолвера выше
    fullName: (parent) => parent.firstName + " " + parent.lastName,
    posts: (parent, args, ctx) => ctx.db.postsByAuthor(parent.id),
  },
};

Заметь: чтобы зарезолвить user.posts, движок берёт parent (объект пользователя) и через его id ищет посты. Так данные «текут» сверху вниз по дереву. Эта структура неслучайна — карта резолверов повторяет карту схемы: для каждого типа из схемы есть свой объект в resolvers, а внутри — функции под имена полей. Благодаря такому соответствию по любому полю схемы легко найти, где оно резолвится, и наоборот. И ещё одна важная мысль: резолвер поля posts совершенно не обязан знать, как был получен сам пользователь. Он принимает готовый parent и делает свою маленькую работу. Эта изоляция — то, что позволяет собирать сложные графы данных из множества крошечных независимых функций.

Как работает под капотом

Если для поля нет явного резолвера, GraphQL применяет дефолтный резолвер: просто берёт у parent свойство с тем же именем (parent.name для поля name). Поэтому скалярные поля часто вообще не нужно писать вручную. Смоделируем движок с дефолтным резолвером и кастомными:

const resolvers = {
  Query: { user: (_p, args, ctx) => ctx.db[args.id] },
  User: {
    // кастомный резолвер: вычисляемое поле
    fullName: (parent) => parent.first + " " + parent.last
  }
};

const ctx = { db: { "42": { id: 42, first: "Аня", last: "К." } } };

function resolveField(typeName, field, parent, args) {
  const custom = resolvers[typeName] && resolvers[typeName][field];
  if (custom) return custom(parent, args, ctx);
  return parent[field];           // дефолтный резолвер
}

const user = resolveField("Query", "user", null, { id: "42" });
console.log(resolveField("User", "first", user, {}));    // "Аня" (дефолт)
console.log(resolveField("User", "fullName", user, {})); // "Аня К." (кастом)

Попробуй сам ▶ — добавь в User резолвер initials: p => p.first[0] + p.last[0] и вызови его. Так появляются вычисляемые поля, которых нет в исходных данных.

Частые ошибки

  • Класть бизнес-логику прямо в резолвер. Резолвер должен быть тонким: достать данные и вернуть. Сложную логику выноси в сервисный слой.
  • Игнорировать parent. Для вложенных полей именно parent подсказывает, чьи именно данные брать (посты этого пользователя).
  • Складывать состояние между запросами. Резолверы должны быть без общего изменяемого состояния; всё «на запрос» живёт в context.

Best practices

  • Группируй резолверы по типам, повторяя схему — так их легко находить и сопоставлять с контрактом.
  • Полагайся на дефолтные резолверы для простых скаляров; пиши вручную только связи и вычисляемые поля.
  • Держи резолверы тонкими: достать данные через сервис/загрузчик из context и вернуть — никакой тяжёлой логики внутри.

Итоги

Резолвер — функция, поставляющая данные для одного поля, с аргументами parent, args, context и info. Они группируются по типам и образуют карту резолверов; скалярные поля часто покрывает дефолтный резолвер. Данные текут по дереву через parent. Дальше посмотрим, как собрать полноценный сервер на Apollo Server 4.

Проверьте себя
1. Что содержит аргумент parent в резолвере?
AАргументы запроса
BРезультат резолвера родительского поля
CТекущего пользователя
DМетаданные запроса
2. Что делает дефолтный резолвер, если для поля нет своего?
AВозвращает null
BБерёт у parent свойство с тем же именем
CБросает ошибку
DДелает запрос в БД