Введение в GraphQL: только самое важное
GraphQL — язык запросов для взаимодействия клиента и сервера, а также среда исполнения этих запросов. Разработан как внутренний проект Facebook в 2012 году, с 2015 года в открытом доступе, с 2018 года развивается GraphQL Foundation в статусе Open Source.
GraphQL был создан для того, чтобы преодолеть ограничения REST-архитектуры. Он подходит для приложений, в которых очень много данных и они хранятся в разных базах.
В основе подхода GraphQL лежит простая идея — вместо того, чтобы создавать конечные точки для каждого объекта, достаточно создать один «умный» эндпойнт, который будет работать со сложными запросами и возвращать клиентам кумулятивные данные в том объеме, в котором клиенты их запрашивают. Такой подход оказался очень удобным, а GraphQL быстро стал популярным.
Содержание:
1. Особенности GraphQL и сравнение с REST API
2. Система типов
3. Схема GraphQL
4. Запросы GraphQL
5. Взаимодействие клиента с сервером
Заключение
1. Особенности GraphQL и сравнение с REST API
Без сравнений с REST API не обойтись, потому что GraphQL изначально разрабатывался как более эффективная альтернатива этой архитектуре.
Но сначала — немного про особенности самого GraphQL:
- Строгая типизация — оценить правильность запроса можно до его выполнения.
- Универсальность — можно использовать с любым фреймворком, языком программирования. Источник данных также не имеет значения.
- Простое масштабирование — позволяет добавлять новые типы и поля, не трогая существующие запросы и не создавая несколько версий одного и того же API.
Кроме того, GraphQL предоставляет одну конечную точку и самодокументируется. Все это обеспечивает удобное использование API без получения лишних данных. Клиенту приходит только та информация, которую он запрашивает.
При использовании REST API контролировать набор получаемых данных гораздо сложнее. Например, если нужно найти всех пользователей с именем Highload, то REST предлагает два подхода:
- Определить эндпойнт, который будет отдавать всех пользователей с именем Highload.
- Определить общий эндпойнт, который будет отдавать всех пользователей, а фильтрацию по имени выполнять самостоятельно уже на стороне клиента.
В первом подходе мы столкнемся с проблемой масштабирования, потому что невозможно создать эндпойнт для каждого пользователя. Или в общем случае — для каждого типа данных, который может понадобиться клиенту. Во втором подходе мы повышаем нагрузку на сеть (гоняя лишние данные) и к тому же вынуждены думать о постобработке на стороне клиента, чтобы отбросить лишние данные.
GraphQL легко решает эти проблемы. Эндпойнт один, лишних данных в ответе нет.
2. Система типов
В основе схемы GraphQL лежит описание типов. Чтобы сделать инструмент универсальным, разработчики предложили Schema Language. Ниже — основные поддерживаемые типы данных.
Объектные
Объектные типы — базовые типы, которые представляют собой объект и набор описывающих его полей.
Пример:
type Planet { id: ID! diameter: Int name: String! population: Float residents: [Person!] }
Скалярные
Поля у объектов могут быть разных типов, но в итоге они должны быть приведены к одному из поддерживаемых скалярных типов:
Int
— 32-битное целое число со знаком.Float
— число двойной точки с плавающей точкой со знаком.String
— строка в кодировке UTF-8.Boolean
—true
илиfalse
.
Есть также специальный скалярный тип ID
. Это уникальный идентификатор, который обычно используют, чтобы получить объект. Он также может быть ключом в кеше.
Значения ID
сериализуется так же, как String
. Но он используется не для отображения клиенту, а только в программах.
Кроме того, во многих имплементациях сервисов GraphQL можно создавать свои скалярные типы.
Аргументы
Аргументы — это пара «ключ — значение», привязанные к полю. Они могут быть литералами и переменными, их можно применять на любых полях. Аргументы должны быть именованными, могут быть опциональными и обязательными.
Например, здесь к полю привязан аргумент id
, который указывает на конкретную книгу:
query { Book(id:"13678hhh") { id title characters { name } } }
Перечисления
Специальный тип, ограниченный набором значений. Например:
enum HAIR_COLOR { BLACK BLONDE BROWN GREY }
Интерфейсы
Интерфейсы — абстрактный тип, который включает набор обязательных полей. В этих полях указываются типы, наследующие интерфейс. Например:
interface Character { id: ID! name: String! } type Animal implements Character { id: ID! name: String! function: String } type Person implements Character { id: ID! name: String! starships: [Starship] }
Есть также другой абстрактный тип, который не включает обязательные поля — union
.
Например:
union SearchResult = Person | City | Planet ... on Person { name height } ... on City { name population manufacturer } ... on Planet { galaxy system }
Union
используется там, где можно вернуть один из перечисленных типов. Например, при реализации поиска.
Query
Query
— тип, который реализует запросы в GraphQL. По сути, это аналог GET
в REST API. Каждый запрос можно представить в качестве строки, которая отправляется в теле HTTP POST
-запрос. Это важное уточнение — все типы запросов в GraphQL отправляются через POST.
Query
описывает, какие данные нужно получить с сервера. Например, у нас есть список пользователей, мы хотим узнать их имена и возраст:
query { users { fname age } }
Ответ приходит в формате JSON. Структура соответствует тому, что клиент запросил в запросе:
data : { users [ { "fname": "High", "age": 39 }, { "fname": "Load", "age": 27 } ] }
При успешном выполнении операции возвращается JSON с ключами data
или error
. Неуспешные операции возвращают JSON с ключом и уведомлением об ошибке. Такой подход упрощает обработку ошибок на стороне клиента.
Mutation
Тип mutation
добавляет данные. Это аналог POST
и PUT
в REST. Например, этот запрос добавит в БД пользователя с именем Highload и возрастом 10.
mutation createUser{ addUser(fname: "Highload", age: 10) { id } }
После выполнения запроса должен прийти ответ с идентификатором записи:
data : { addUser : "f374uw" }
Subscription
Тип subscription
позволяет слушать изменения в базе данных в реалтайме. Подписки используют для этого вебсокеты.
Пример:
subscription listenLikes { listenLikes { fname likes } }
Этот запрос позволяет получить список пользователей с именами и количеством лайков каждый раз, когда количество лайков меняется. Подобная функциональность используется, например, в веб-интерфейсе для отображения активности пользователей или результатов голосования.
3. Схема GraphQL
В основе GraphQL API используется схема. Она описывает, какие типы данных поддерживаются и какие типы данных возвращаются в ответ на запрос.
Работа с сервером GraphQL всегда начинается с создания схемы. Она включает два взаимосвязанных объекта: TypeDefs
и Resolvers
.
TypeDefs
В схеме необходимо определить типы, с которым может работать клиент. Это делается с помощью объекта TypeDefs
.
const typeDefs = gql` type User { id: Int fname: String age: Int likes: Int posts: [Post] } type Post { id: Int user: User body: String } type Query { users(id: Int!): User! posts(id: Int!): Post! } type Mutation { incrementLike(fname: String!) : [User!] } type Subscription { listenLikes : [User] } `;
Сначала мы определяем тип User
. Для него доступны данные id
, fname
, age
, likes
, posts
. Это значит, что клиент может получить, например, возраст пользователя или увидеть все опубликованные этим пользователем посты.
Есть также тип Post
, у которого указаны id
, user
и body
.
Для каждого поля определяется тип данных. Всего GraphQL поддерживает четыре типа данных: String
, Int, Float
, Boolean
. Если добавить в поле восклицательный знак, то оно становится обязательным.
В схеме также определены типы Query
, Mutation
и Subscription
.
- Внутри
Query
два типа —users
иposts
. Это обязательные поля. Работают они одинаково — принимаютid
и возвращают пользователя с соответствующим идентификатором. - В
Mutation
располагается типincrementLike
. Он принимает параметр fname и возвращает перечень пользователей. - В
Subscription
располагаетсяlistenLikes
. В этом примере он также возвращает список пользователей.
После определения типов добавляется логика, которая определяет, как сервер будет отвечать на запросы клиентов. За эту часть работы отвечают распознаватели (Resolvers
).
Resolvers
Resolver
— функция, возвращающая данные для конкретного поля. Вернуть можно только те данные, которые определены в схеме объектом TypeDefs
. Распознаватели могут быть асинхронными.
Пример функций-распознавателей:
const resolvers = { Query: { // Вернуть объект пользователя, который соответствует переданному id users(root, args) { return users.filter(user => user.id === args.id)[0] }, // Вернуть объект поста, который соответствует переданному id posts(root, args) { return posts.filter(post => post.id === args.id)[0] } }, User: { // Принять данные пользователя и вернуть его посты posts: (user) => { return posts.filter(post => post.userId === user.id) } }, Post: { // Принять данные поста и вернуть пользователя, который его опубликовал user: (post) => { return users.filter(user => user.id === post.userId)[0] } }, Mutation: { // Изменить объект users: увеличить количество лайков //для пользователя с выбранным fname incrementLike(parent, args) { users.map((user) => { if(user.fname === args.fname) user.likes++ return user }) pubsub.publish('LIKES', {listenLikes: users}); return users } }, Subscription: { // Слушать LIKES и отвечать при обновлении pubsub listenLikes: { subscribe: () => pubsub.asyncIterator(['LIKES']) } } };
Это пример простой схемы, которая определяет, как и какие данные будет получать клиент от GraphQL-сервера.
4. Запросы GraphQL
Схема определяет, какие данные может получить клиент. Но для взаимодействия с ней нужно составлять правильные запросы. Здесь GraphQL тоже предлагает продуманную концепцию.
Поля
Пример простого запроса:
{ user { name } }
Ответ на запрос выглядит так:
{ "data": { "user": { "name": "highload" } } }
В запросе два поля. Поле user
вернуло объект с типом String
.
Аргументы
В запросе можно указать на конкретного пользователя, используя аргумент. Например:
{ user(id: "2") { age } }
Здесь мы хотим получить возраст пользователя с id = 2.
Вместо id
в качестве аргумента можно использовать name, если мы знаем, что в схеме описана функция для обработки такого значения.
Можно использовать несколько аргументов. Например, установить лимит на количество постов:
{ user(id: "2") { name posts(limit: 10) } }
Псевдонимы
Для удобства можно назначать полям алиасы — псевдонимы. Пример запроса:
{ accholder: user(id: "22") { firstname: name } }
В ответе будет указан псевдоним:
{ "data": { "accholder": { "firstname": "high" } } }
Алиасы помогают избежать конфликтов при совпадении названий полей.
Фрагменты
Фрагменты помогают указать структуру со многими полями. Концепция используется, когда нужно разделить сложные запросы на мелкие части. Особенно это полезно, когда приходится объединять большое число компонентов интерфейса с различными фрагментами в одну выборку.
Например:
fragment comparisonFields on tweet { userName userHandle date body repliesCount likes }
Ответ:
"data": { "sample": { userName: "highload", userHandle: "@highload", date: "2022-05-01", body: "Good", repliesCount: 11, tweetsCount: 300, likes: 568, },
Переменные
Значения внутри запроса можно сделать динамическими, используя переменные. Пример запроса:
query GetAccHolder($id: String) { accholder: user(id: $id) { fullname: name } } { "id": "1" }
Здесь мы создаем именованную функцию. Это может быть полезным в приложениях, в которых выполняется много запросов.
Можно задать значение переменной по умолчанию:
query GetAccHolder($id: String = "1") { accholder: user(id: $id) { fullname: name } }
И пометить переменную как обязательную, добавив к ней восклицательный знак:
query GetAccHolder($id: String!) { accholder: user(id: $id) { fullname: name }
Директивы
Концепция переменных позволяет использовать директивы, которые динамически меняют структуру и форму запросов в зависимости от условий. В GraphQL используются две директивы: @include
и @skip
.
Пример использования @include
:
query GetFollowers($id: String) { user(id: $id) { fullname: name, followers: @include(if: $getFollowers) { name userHandle tweets } } } { "id": "1", "$getFollowers": false }
Здесь мы говорим, что нужно использовать поле, если это true
. Видим, что у $getFollowers
значение false
, поэтому поле не включается в ответ.
Пример использования @skip
:
query GetFollowers($id: String) { user(id: $id) { fullname: name, followers: @skip(if: $getFollowers) { name userHandle tweets } } } { "id": "1", "$getFollowers": true }
Здесь мы говорим, что нужно пропустить поле, если это true
. Видим, что у $getFollowers
значение true
, поэтому поле не включается в ответ.
5. Взаимодействие клиента с сервером
Кратко порядок взаимодействия можно описать так:
- Клиент отправляет на GraphQL-сервер запрос на чтение или изменение данных. Этот запрос составлен в соответствии с утвержденной заранее схемой.
- GraphQL-сервер распознает запрос по специальным функциям — резолверам — и получает данные по запрашиваемым полям.
- Клиент получает ответ с запрашиваемой структурой данных, обычно в формате JSON.
Клиенту не важно, откуда поступают запрошенные им данные. Он делает запрос в нужном ему объеме к серверу GraphQL. Сервер может работать с любыми источниками: базами данных, результатами поиска, Docker-контейнерами.
Использование GraphQL делает разделение между фронтендом и бэкендом еще более четким. После согласования схемы фронтендерам больше не придется просить бэкендеров создать еще один эндпойнт или добавить к имеющимся дополнительные параметры. Схема описывается один раз. Дальше фронтендеры сами создают запросы и комбинируют их так, чтобы решать свои задачи.
Заключение
Мы разобрались с GraphQL, что это язык запросов, посмотрели систему типов, пример схемы и порядок общения между клиентами и сервером. Также мы сравнили GraphQL API и REST API.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: