
Разбор запросов GraphQL от простого к сложному — queries и mutations, аргументы, переменные, вложенные схемы, подписки. С примерами и кодом схемы.
Привет, Хабр!
Про суть GraphQL написано много, про сами запросы меньше. С них и начнем — от простого запроса к вложенным схемам и подпискам.
Среды тестирования
- GraphiQL — встроенный пакет для тестирования на сервере.
- Apollo Client Developer Tools — тот же GraphiQL в виде расширения Chrome.
Документация схемы
Схема сама себя документирует. После изменений на сервере справка по запросам обновляется автоматически и подсвечивает ошибки в самих запросах.

Два базовых типа операций — queries (чтение) и mutations (запись).
Queries
Запросом обращаемся к полю user и перечисляем, какие поля вернуть — firstName и lastName.
query myQuery { user { firstName lastName } }
В GraphiQL ключевое слово query перед скобками можно опустить. На клиенте тип операции нужно указывать явно, иначе запрос не соберется.
Mutations
Мутация меняет данные. Указываем операцию и ее аргументы.
mutation myMutation { userCreate(firstName: "Jane", lastName: "Doe") }
Queries выполняются параллельно, mutations — последовательно, сверху вниз.
Subscriptions
Третий тип операций — подписки. Синтаксис как у queries, но вместо разового ответа клиент держит поток обновлений.
subscription mySubscription { user { firstName lastName } }
Query и mutation отвечают один раз и закрывают запрос. Subscription живет в постоянном канале, обычно поверх WebSocket, и шлет данные по мере событий на сервере.
Аргументы
Аргументы подставляют значения в запрос и работают одинаково для query, mutation и subscription. Сначала объявляем аргумент, потом применяем к полю.
query myQuery($id: ID) { user(id: $id) { firstName lastName } }
Запрос найдет пользователя с нужным id и вернет firstName и lastName.
Переменные
Значение аргумента передается отдельным блоком переменных. Тип переменной объявляется в сигнатуре запроса и должен совпадать с типом поля в схеме.

query myQuery($id: ID!) { user(id: $id) { firstName lastName } }
Блок переменных:
{ "id": "595fdbe2cc86ed070ce1da52" }
Восклицательный знак в ID! делает поле обязательным.
На клиенте запрос формировать руками не нужно. Под популярные фреймворки есть готовые клиенты Apollo — vue-apollo, apollo/client для React, apollo-angular.
Вложенные запросы
Плоские поля — простой случай, реальные схемы вложенные. Пример на mongoose и graphql-js из проекта того времени.
Без вложенности:
// MongoDB schema const schema = new mongoose.Schema({ firstName: { type: String }, lastName: { type: String }, }) export const USER_MODEL = mongoose.model('users', schema) // GraphQL type const user = { firstName: { type: GraphQLString }, lastName: { type: GraphQLString }, } export const USER = new GraphQLObjectType({ name: 'User', fields: user, })
С вложенной схемой:
// MongoDB schema const schema = new mongoose.Schema({ firstName: { type: String }, lastName: { type: String }, secure: { public_key: { type: String }, private_key: { type: String }, }, }) // ... const secure = new GraphQLObjectType({ name: 'Secure', fields: { public_key: { type: GraphQLString }, }, }) export const USER = new GraphQLObjectType({ name: 'User', fields: { firstName: { type: GraphQLID }, secure: { type: secure }, }, })
Запрос по вложенному полю:
query myQuery($id: ID!) { user(id: $id) { firstName secure { public_key } } }
Мутация возвращает объект
Мутация не обязана возвращать true/false. Если типом ответа поставить тип из схемы вместо GraphQLBoolean, мутация вернет объект, как обычный query.

mutation auth($email: String!, $password: String!) { auth(email: $email, password: $password) { id secure { public_key } } }
Где подписки реально пригодились
Синтаксис подписок простой, интереснее то, где они окупаются. В realtime-приложении на Vue Apollo и Apollo Server клиент и сервер держали постоянный канал по WebSocket и синхронизировали состояние на лету. Каталог с конечными позициями — забрали последний экземпляр, и он сразу пропадает у всех, у кого открыт список. Сделано на GraphQL-подписках Apollo поверх Redis PubSub, чтобы подписки жили на нескольких инстансах, даже если событие отработало на одном.
Ключевое отличие подписки от query не в синтаксисе, а в транспорте. Query отвечает один раз по HTTP, subscription держит соединение и требует WebSocket-слоя на сервере и клиенте, плюс шину событий, если инстансов больше одного.
Чего статья не покрывала
Статья 2017 года про форму запросов. За ее рамками осталось то, обо что упираешься на нагрузке.
- N+1 на резолверах. Вложенный запрос по списку из N элементов легко превращается в N+1 обращений к базе — резолвер дергается на каждый элемент. Лечится батчингом на уровне резолверов, DataLoader собирает запросы за один тик в один запрос к базе. Тогда я об этом не думал, и это первое, что добавил бы сегодня.
- Защита схемы. Клиент сам задает форму и глубину запроса, поэтому глубокий вложенный запрос — это вектор нагрузки. В проде нужны лимит глубины и сложности запроса и отключенная интроспекция. В статье этого нет, тогда не задавался вопросом.
Заключение
Базовые запросы GraphQL — queries, mutations, subscriptions, аргументы, переменные, вложенность — закрывают почти все, что нужно на старте. Синтаксисом список не заканчивается, на нагрузке добавляются N+1, защита схемы и транспорт под подписки.