Единый эндпоинт и анатомия запроса

Весь GraphQL общается через один URL и понимает три типа операций — query, mutation, subscription. А любой ответ всегда укладывается в два ключа: data и errors.

Один адрес, три глагола, предсказуемый конверт ответа — выучив эту анатомию, ты перестанешь путаться в любом GraphQL API.

В REST у тебя десятки URL и набор HTTP-методов: GET, POST, PUT, DELETE. В GraphQL вместо этого один эндпоинт (по соглашению /graphql) и три операции уровня языка:

  • query — чтение данных (аналог GET);
  • mutation — изменение данных: создать, обновить, удалить (аналог POST/PUT/DELETE);
  • subscription — подписка на поток событий в реальном времени (обычно поверх WebSocket).

Технически запросы и мутации почти всегда отправляются HTTP-методом POST с телом, где лежит текст операции. Сам HTTP-метод тут не несёт смысла — смысл несёт ключевое слово операции в теле. Это важный сдвиг мышления: в REST «глагол» действия — это HTTP-метод (GET, POST, PUT), и маршрутизатор сервера смотрит именно на него вместе с путём. В GraphQL транспорт (HTTP) и семантика операции разведены: транспорт почти всегда один и тот же POST, а что именно делать — читать, писать или подписываться — определяет первое слово в теле запроса. Поэтому переходя с REST на GraphQL, перестань искать «правильный URL и метод» под каждое действие: и поиск пользователя, и его создание уходят на один и тот же адрес, отличаясь лишь словом query или mutation.

Разбираем запрос по частям

query GetUser($id: ID!) {   # тип операции, имя, переменные
  user(id: $id) {           # поле с аргументом
    name                    # скалярное поле
    posts(first: 3) {       # вложенное поле + аргумент
      title
      likes
    }
  }
}

Здесь: query — тип операции; GetUser — имя (необязательно, но помогает в отладке и логах); $id: ID! — объявление переменной; user, posts — поля; в скобках — аргументы; name, title — листья дерева (скаляры). Запрос — это всегда дерево полей.

Конверт ответа

Каким бы ни был запрос, ответ — это JSON ровно с двумя возможными ключами верхнего уровня: data (то, что удалось получить) и errors (массив ошибок, если что-то пошло не так). Важная особенность: ответ может содержать и data, и errors одновременно — часть полей вернулась, часть упала.

{
  "data": { "user": { "name": "Аня", "posts": null } },
  "errors": [
    { "message": "Нет доступа к постам", "path": ["user", "posts"] }
  ]
}

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

Сервер получает тело запроса, выбирает корневой тип по ключевому слову операции (Query, Mutation или Subscription), а дальше обходит дерево полей. Каждое поле резолвится своей функцией, результат складывается в data по тому же пути, что и в запросе. Ошибка в конкретном поле не роняет весь ответ — она пишется в errors с указанием path, а на месте поля оказывается null.

Смоделируем разбор простого запроса в JS: распарсим список запрошенных полей и соберём ответ-дерево.

const root = {
  user: { name: "Аня", city: "Казань", secret: "пароль123" }
};

// "запрос": какие поля user мы хотим
const ask = { user: ["name", "city"] };

function resolve(ask, root) {
  const data = {};
  for (const key in ask) {
    const obj = root[key];
    data[key] = {};
    for (const f of ask[key]) data[key][f] = obj[f];
  }
  return { data };
}

console.log(JSON.stringify(resolve(ask, root), null, 2));
// secret в ответ не попал — мы его не просили

Попробуй сам ▶ — добавь "secret" в массив полей и убедись, что теперь он приедет. Клиент управляет тем, что видит.

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

  • Искать «другой URL» под мутацию. Эндпоинт тот же — меняется лишь ключевое слово операции.
  • Ждать HTTP-статус 404 или 500 при ошибке. GraphQL почти всегда отвечает 200 OK, а ошибки кладёт в массив errors. Это сбивает с толку тех, кто привык к REST-статусам.
  • Запрашивать объектное поле без вложенных полей. Нельзя написать просто user — у объекта надо указать, какие именно поля внутри тебе нужны.

Best practices

  • Всегда давай операциям имена (query GetUser): это улучшает логи, метрики и трассировку на сервере.
  • Проверяй оба ключа ответа — и data, и errors. Частичный успех — норма для GraphQL.
  • Выноси значения в переменные ($id), а не зашивай в текст запроса: так запрос становится переиспользуемым и кэшируемым.

Итоги

GraphQL — это один эндпоинт, три операции (query, mutation, subscription) и предсказуемый конверт ответа из ключей data и errors. Запрос всегда дерево полей с аргументами; ошибки не роняют весь ответ, а помечают конкретный путь. Эту анатомию мы будем наполнять смыслом в следующих разделах — начиная со схемы и типов.

Проверьте себя
1. Сколько типов операций определяет GraphQL и какие?
AДва: GET и POST
BТри: query, mutation, subscription
CЧетыре: GET, POST, PUT, DELETE
DОдин: query
2. Какой HTTP-статус обычно у GraphQL-ответа с ошибкой в одном из полей?
A404 Not Found
B500 Internal Server Error
C200 OK, а ошибка лежит в массиве errors
D400 Bad Request всегда