Создание вашего первого GraphQL API

Опубликовано: 2022-03-11

Предисловие

Несколько лет назад Facebook представил новый способ создания внутренних API-интерфейсов под названием GraphQL, который в основном представляет собой предметно-ориентированный язык для запросов и манипулирования данными. Поначалу я не обращал на это особого внимания, но в итоге оказался вовлеченным в проект в Toptal, где мне нужно было реализовать back-end API на основе GraphQL. Именно тогда я пошел дальше и научился применять знания, полученные для REST, к GraphQL.

Это был очень интересный опыт, и в период реализации мне пришлось переосмыслить стандартные подходы и методологии, используемые в REST API, в более удобном для GraphQL виде. В этой статье я попытаюсь обобщить общие проблемы, которые следует учитывать при первом внедрении API GraphQL.

Требуемые библиотеки

GraphQL был разработан внутри Facebook и публично выпущен в 2015 году. Позже, в 2018 году, проект GraphQL был перенесен из Facebook в недавно созданный GraphQL Foundation, размещенный некоммерческой организацией Linux Foundation, которая поддерживает и разрабатывает спецификацию языка запросов GraphQL и справочник. реализация для JavaScript.

Поскольку GraphQL все еще является молодой технологией, а первоначальная эталонная реализация была доступна для JavaScript, наиболее зрелые библиотеки для нее существуют в экосистеме Node.js. Есть также две другие компании, Apollo и Prisma, которые предоставляют инструменты и библиотеки с открытым исходным кодом для GraphQL. Пример проекта в этой статье будет основан на эталонной реализации GraphQL для JavaScript и библиотек, предоставленных этими двумя компаниями:

  • Graphql-js — эталонная реализация GraphQL для JavaScript.
  • Apollo-server — сервер GraphQL для Express, Connect, Hapi, Koa и др.
  • Apollo-graphql-tools — создавайте, имитируйте и сшивайте схему GraphQL с помощью SDL.
  • Prisma-graphql-middleware — разделите свои преобразователи GraphQL на функции промежуточного программного обеспечения.

В мире GraphQL вы описываете свои API с помощью схем GraphQL, и для них спецификация определяет собственный язык, называемый языком определения схем GraphQL (SDL). SDL очень прост и интуитивно понятен в использовании, но в то же время является чрезвычайно мощным и выразительным.

Существует два способа создания схем GraphQL: подход «сначала код» и подход «сначала схема».

  • В подходе «сначала код» вы описываете свои схемы GraphQL как объекты JavaScript на основе библиотеки graphql-js, а SDL автоматически генерируется из исходного кода.
  • В подходе, основанном на схеме, вы описываете свои схемы GraphQL в SDL и подключаете свою бизнес-логику с помощью библиотеки графических инструментов Apollo.

Лично я предпочитаю подход «сначала схема» и буду использовать его для примера проекта в этой статье. Мы реализуем пример классического книжного магазина и создадим серверную часть, которая предоставит API CRUD для создания авторов и книг, а также API для управления пользователями и аутентификации.

Создание базового сервера GraphQL

Чтобы запустить базовый сервер GraphQL, мы должны создать новый проект, инициализировать его с помощью npm и настроить Babel. Чтобы настроить Babel, сначала установите необходимые библиотеки с помощью следующей команды:

 npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node

После установки Babel создайте файл с именем .babelrc в корневом каталоге нашего проекта и скопируйте туда следующую конфигурацию:

 { "presets": [ [ "@babel/env", { "targets": { "node": "current" } } ] ] }

Также отредактируйте файл package.json и добавьте следующую команду в раздел scripts :

 { ... "scripts": { "serve": "babel-node index.js" }, ... }

После того, как мы настроили Babel, установите необходимые библиотеки GraphQL с помощью следующей команды:

 npm install --save express apollo-server-express graphql graphql-tools graphql-tag

После установки необходимых библиотек, чтобы запустить сервер GraphQL с минимальной настройкой, скопируйте этот фрагмент кода в наш файл index.js :

 import gql from 'graphql-tag'; import express from 'express'; import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'; const port = process.env.PORT || 8080; // Define APIs using GraphQL SDL const typeDefs = gql` type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } }; // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolvers maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });

После этого мы можем запустить наш сервер с помощью команды npm run serve , и если мы перейдем в веб-браузере по URL-адресу http://localhost:8080/graphql , откроется интерактивная визуальная оболочка GraphQL под названием Playground, где мы можем выполнять запросы и мутации GraphQL и просматривать данные результатов.

В мире GraphQL функции API делятся на три набора, называемые запросами, мутациями и подписками:

  • Запросы используются клиентом для запроса необходимых ему данных с сервера.
  • Мутации используются клиентом для создания/обновления/удаления данных на сервере.
  • Подписки используются клиентом для создания и поддержания подключения к серверу в режиме реального времени. Это позволяет клиенту получать события с сервера и действовать соответствующим образом.

В нашей статье мы обсудим только запросы и мутации. Подписки — это огромная тема — они заслуживают отдельной статьи и не требуются в каждой реализации API.

Расширенные скалярные типы данных

Очень скоро после того, как вы поиграетесь с GraphQL, вы обнаружите, что SDL предоставляет только примитивные типы данных, а расширенные скалярные типы данных, такие как Date, Time и DateTime, которые являются важной частью каждого API, отсутствуют. К счастью, у нас есть библиотека, которая помогает нам решить эту проблему, и она называется graphql-iso-date. После его установки нам нужно будет определить новые расширенные скалярные типы данных в нашей схеме и подключить их к реализациям, предоставляемым библиотекой:

 import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date'; // Define APIs using GraphQL SDL const typeDefs = gql` scalar Date scalar Time scalar DateTime type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Date: GraphQLDate, Time: GraphQLTime, DateTime: GraphQLDateTime, Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } };

Наряду с датой и временем существуют и другие интересные реализации скалярного типа данных, которые могут быть вам полезны в зависимости от вашего варианта использования. Например, одним из них является graphql-type-json, который дает нам возможность использовать динамическую типизацию в нашей схеме GraphQL и передавать или возвращать нетипизированные объекты JSON с помощью нашего API. Также существует библиотека graphql-scalar, которая дает нам возможность определять собственные скаляры GraphQL с расширенной очисткой/проверкой/преобразованием.

При необходимости вы также можете определить собственный скалярный тип данных и использовать его в своей схеме, как показано выше. Это несложно, но обсуждение этого выходит за рамки данной статьи — если интересно, вы можете найти более подробную информацию в документации Apollo.

Схема разделения

После добавления функциональности в вашу схему, она начнет расти и мы поймем, что невозможно держать весь набор определений в одном файле, и нам нужно разбить его на маленькие части, чтобы организовать код и сделать его более масштабируемым для большего размера. К счастью, функция построения схемы makeExecutableSchema , предоставляемая Apollo, также принимает определения схемы и карты преобразователей в виде массива. Это дает нам возможность разделить нашу схему и карту распознавателей на более мелкие части. Это именно то, что я сделал в своем примере проекта; Я разделил API на следующие части:

  • auth.api.graphql — API для аутентификации и регистрации пользователей.
  • author.api.graphql — CRUD API для записей авторов
  • book.api.graphql — CRUD API для записей книг
  • root.api.graphql — корень схемы и общие определения (например, расширенные скалярные типы)
  • user.api.graphql — CRUD API для управления пользователями

Во время схемы расщепления есть одна вещь, которую мы должны учитывать. Одна из частей должна быть корневой схемой, а другие должны расширять корневую схему. Это звучит сложно, но на самом деле это довольно просто. В корневой схеме запросы и мутации определяются следующим образом:

 type Query { ... } type Mutation { ... }

А в других они определяются так:

 extend type Query { ... } extend type Mutation { ... }

И это все.

Аутентификация и авторизация

В большинстве реализаций API требуется ограничить глобальный доступ и предоставить какие-либо политики доступа на основе правил. Для этого мы должны ввести в наш код: Authentication — для подтверждения личности пользователя — и Authorization для обеспечения соблюдения политик доступа на основе правил.

В мире GraphQL, как и в мире REST, обычно для аутентификации мы используем веб-токен JSON. Для валидации переданного токена JWT нам нужно перехватывать все входящие запросы и проверять на них заголовок авторизации. Для этого при создании сервера Apollo мы можем зарегистрировать функцию как контекстный хук, который будет вызываться с текущим запросом, создающим контекст, общий для всех распознавателей. Это можно сделать следующим образом:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, context: ({ req, res }) => { const context = {}; // Verify jwt token const parts = req.headers.authorization ? req.headers.authorization.split(' ') : ['']; const token = parts.length === 2 && parts[0].toLowerCase() === 'bearer' ? parts[1] : undefined; context.authUser = token ? verify(token) : undefined; return context; } }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });

Здесь, если пользователь передаст правильный токен JWT, мы проверяем его и сохраняем объект пользователя в контексте, который будет доступен для всех распознавателей во время выполнения запроса.

Мы подтвердили личность пользователя, но наш API по-прежнему доступен глобально, и ничто не мешает нашим пользователям вызывать его без авторизации. Один из способов предотвратить это — проверять пользовательский объект в контексте непосредственно в каждом распознавателе, но это очень подверженный ошибкам подход, потому что нам приходится писать много стандартного кода, и мы можем забыть добавить проверку при добавлении нового распознавателя. . Если мы посмотрим на фреймворки REST API, то, как правило, такого рода проблемы решаются с помощью перехватчиков HTTP-запросов, но в случае с GraphQL это не имеет смысла, потому что один HTTP-запрос может содержать несколько запросов GraphQL, и если мы еще добавим мы получаем доступ только к необработанному строковому представлению запроса и должны анализировать его вручную, что определенно не является хорошим подходом. Эта концепция плохо переносится с REST на GraphQL.

Итак, нам нужен какой-то способ перехвата запросов GraphQL, и этот способ называется prisma-graphql-middleware. Эта библиотека позволяет запускать произвольный код до или после вызова преобразователя. Это улучшает структуру нашего кода, обеспечивая повторное использование кода и четкое разделение задач.

Сообщество GraphQL уже создало множество замечательных промежуточных программ на основе библиотеки промежуточных программ Prisma, которые решают некоторые конкретные варианты использования, а для авторизации пользователей существует библиотека под названием graphql-shield, которая помогает нам создать уровень разрешений для нашего API.

После установки graphql-shield мы можем ввести уровень разрешений для нашего API следующим образом:

 import { allow } from 'graphql-shield'; const isAuthorized = rule()( (obj, args, { authUser }, info) => authUser && true ); export const permissions = { Query: { '*': isAuthorized, sayHello: allow }, Mutation: { '*': isAuthorized, sayHello: allow } }

И мы можем применить этот слой в качестве промежуточного программного обеспечения к нашей схеме следующим образом:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

Здесь при создании объекта щита мы устанавливаем для allowExternalErrors значение true, потому что по умолчанию поведение щита состоит в том, чтобы перехватывать и обрабатывать ошибки, возникающие внутри распознавателей, а это было неприемлемым поведением для моего примера приложения.

В приведенном выше примере мы ограничили доступ к нашему API только для аутентифицированных пользователей, но щит очень гибкий, и с его помощью мы можем реализовать очень богатую схему авторизации для наших пользователей. Например, в нашем примере приложения у нас есть две роли: USER и USER_MANAGER , и только пользователи с ролью USER_MANAGER могут вызывать функции администрирования пользователей. Это реализовано так:

 export const isUserManager = rule()( (obj, args, { authUser }, info) => authUser && authUser.role === 'USER_MANAGER' ); export const permissions = { Query: { userById: isUserManager, users: isUserManager }, Mutation: { editUser: isUserManager, deleteUser: isUserManager } }

Еще одна вещь, которую я хочу упомянуть, это то, как организовать функции промежуточного программного обеспечения в нашем проекте. Как и в случае с определениями схем и картами распознавателей, лучше разделить их по схемам и хранить в отдельных файлах, но в отличие от сервера Apollo, который принимает массивы определений схем и карт распознавателей и сшивает их для нас, библиотека промежуточного программного обеспечения Prisma этого не делает и принимает только один объект карты промежуточного программного обеспечения, поэтому, если мы разделим их, нам придется сшивать их обратно вручную. Чтобы увидеть мое решение этой проблемы, см. класс ApiExplorer в примере проекта.

Проверка

GraphQL SDL предоставляет очень ограниченную функциональность для проверки пользовательского ввода; мы можем только определить, какое поле является обязательным, а какое необязательным. Любые дальнейшие требования проверки мы должны реализовать вручную. Мы можем применять правила проверки непосредственно в функциях распознавателя, но эта функциональность действительно здесь неуместна, и это еще один отличный вариант использования промежуточного программного обеспечения GraphQL для пользователей. Например, давайте использовать входные данные запроса регистрации пользователя, где мы должны проверить, является ли имя пользователя правильным адресом электронной почты, совпадают ли введенные пароли и достаточно ли надежен пароль. Это можно реализовать так:

 import { UserInputError } from 'apollo-server-express'; import passwordValidator from 'password-validator'; import { isEmail } from 'validator'; const passwordSchema = new passwordValidator() .is().min(8) .is().max(20) .has().letters() .has().digits() .has().symbols() .has().not().spaces(); export const validators = { Mutation: { signup: (resolve, parent, args, context) => { const { email, password, rePassword } = args.signupReq; if (!isEmail(email)) { throw new UserInputError('Invalid Email address!'); } if (password !== rePassword) { throw new UserInputError('Passwords don\'t match!'); } if (!passwordSchema.validate(password)) { throw new UserInputError('Password is not strong enough!'); } return resolve(parent, args, context); } } }

И мы можем применить слой валидаторов в качестве промежуточного программного обеспечения к нашей схеме вместе со слоем разрешений следующим образом:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, validators, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app })

N + 1 запрос

Еще одна проблема, которую следует учитывать, которая возникает с API-интерфейсами GraphQL и часто упускается из виду, — это запросы N + 1. Эта проблема возникает, когда у нас есть отношения «один ко многим» между типами, определенными в нашей схеме. Чтобы продемонстрировать это, например, воспользуемся книжным API нашего примера проекта:

 extend type Query { books: [Book!]! ... } extend type Mutation { ... } type Book { id: ID! creator: User! createdAt: DateTime! updatedAt: DateTime! authors: [Author!]! title: String! about: String language: String genre: String isbn13: String isbn10: String publisher: String publishDate: Date hardcover: Int } type User { id: ID! createdAt: DateTime! updatedAt: DateTime! fullName: String! email: String! }

Здесь мы видим, что тип User имеет отношение «один ко многим» с типом Book , и это отношение представлено как поле Creator в Book . Карта распознавателей для этой схемы определяется следующим образом:

 export const resolvers = { Query: { books: (obj, args, context, info) => { return bookService.findAll(); }, ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { return userService.findById(creatorId); }, ... } }

Если мы выполним запрос книг с помощью этого API и посмотрим на журнал операторов SQL, мы увидим что-то вроде этого:

 select `books`.* from `books` select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? ...

Нетрудно догадаться — во время выполнения сначала вызывался распознаватель для запроса books, который возвращал список книг, а затем каждый объект книги вызывался распознавателем поля создателя, и такое поведение вызывало N + 1 запросов к базе данных. Если мы не хотим взорвать нашу базу данных, такое поведение не очень хорошо.

Для решения проблемы N+1 запросов разработчики Facebook создали очень интересное решение под названием DataLoader, которое описано на его странице README так:

«DataLoader — это универсальная утилита, которую можно использовать как часть уровня выборки данных вашего приложения, чтобы обеспечить упрощенный и согласованный API для различных удаленных источников данных, таких как базы данных или веб-сервисы, посредством пакетной обработки и кэширования».

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

В нашем примере проекта DataLoader определен следующим образом для поля Creator:

 export class UserDataLoader extends DataLoader { constructor() { const batchLoader = userIds => { return userService .findByIds(userIds) .then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) ); }; super(batchLoader); } static getInstance(context) { if (!context.userDataLoader) { context.userDataLoader = new UserDataLoader(); } return context.userDataLoader; } }

После того, как мы определили UserDataLoader, мы можем изменить преобразователь поля создателя следующим образом:

 export const resolvers = { Query: { ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { const userDataLoader = UserDataLoader.getInstance(context); return userDataLoader.load(creatorId); }, ... } }

После внесенных изменений, если мы снова выполним запрос books и посмотрим на журнал операторов SQL, мы увидим что-то вроде этого:

 select `books`.* from `books` select `users`.* from `users` where `id` in (?)

Здесь мы видим, что N + 1 запросов к базе данных были сокращены до двух запросов, где первый выбирает список книг, а второй выбирает список пользователей, представленных как создатели в списке книг. Теперь давайте объясним, как DataLoader достигает этого результата.

Основной функцией DataLoader является пакетная обработка. Во время одной фазы выполнения DataLoader соберет все отдельные идентификаторы всех вызовов отдельных функций загрузки, а затем вызовет пакетную функцию со всеми запрошенными идентификаторами. Важно помнить, что экземпляры DataLoaders нельзя использовать повторно, после вызова пакетной функции возвращаемые значения будут кэшироваться в экземпляре навсегда. Из-за такого поведения мы должны создавать новый экземпляр DataLoader для каждой фазы выполнения. Для этого мы создали статическую функцию getInstance , которая проверяет, представлен ли экземпляр DataLoader в объекте контекста, и, если не найден, создает его. Помните, что новый объект контекста создается для каждой фазы выполнения и используется всеми преобразователями.

Функция пакетной загрузки DataLoader принимает массив различных запрошенных идентификаторов и возвращает обещание, которое преобразуется в массив соответствующих объектов. При написании функции пакетной загрузки мы должны помнить две важные вещи:

  1. Массив результатов должен быть той же длины, что и массив запрошенных идентификаторов. Например, если мы запросили идентификаторы [1, 2, 3] , возвращаемый массив результатов должен содержать ровно три объекта: [{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
  2. Каждый индекс в массиве результатов должен соответствовать такому же индексу в массиве запрошенных идентификаторов. Например, если массив запрошенных идентификаторов имеет следующий порядок: [3, 1, 2] , то возвращаемый массив результатов должен содержать объекты точно в таком же порядке: [{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]

В нашем примере мы гарантируем, что порядок результатов соответствует порядку запрошенных идентификаторов с помощью следующего кода:

 then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) )

Безопасность

И последнее, но не менее важное, я хочу упомянуть о безопасности. С помощью GraphQL мы можем создавать очень гибкие API и предоставлять пользователю широкие возможности для запроса данных. Это наделяет клиентскую часть приложения довольно большими возможностями, и, как сказал дядя Бен, «с большой силой приходит большая ответственность». Без надлежащей безопасности злоумышленник может отправить дорогостоящий запрос и вызвать DoS-атаку (отказ в обслуживании) на нашем сервере.

Первое, что мы можем сделать для защиты нашего API, — отключить самоанализ схемы GraphQL. По умолчанию сервер API GraphQL предоставляет возможность интроспекции всей своей схемы, которая обычно используется интерактивными визуальными оболочками, такими как GraphiQL и Apollo Playground, но также может быть очень полезно для злоумышленника для создания сложного запроса на основе нашего API. . Мы можем отключить это, установив для параметра introspection значение false при создании сервера Apollo:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

Следующее, что мы можем сделать для защиты нашего API, — это ограничить глубину запроса. Это особенно важно, если у нас есть циклическая связь между нашими типами данных. Например, в нашем примере тип проекта « Author » содержит полевые книги, а тип Book » — полевые авторы. Это явно циклическая связь, и ничто не мешает злоумышленнику написать такой запрос:

 query { authors { id, fullName books { id, title authors { id, fullName books { id, title, authors { id, fullName books { id, title authors { ... } } } } } } } }

Понятно, что при достаточной вложенности такой запрос запросто может взорвать наш сервер. Чтобы ограничить глубину запросов, мы можем использовать библиотеку под названием graphql-depth-limit. После установки мы можем применить ограничение глубины при создании сервера Apollo, например:

 // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false, validationRules: [ depthLimit(5) ] }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })

Здесь мы ограничили максимальную глубину запросов пятью.

Post Scriptum: переход с REST на GraphQL интересен

В этом руководстве я попытался продемонстрировать распространенные проблемы, с которыми вы столкнетесь, когда начнете внедрять API GraphQL. Однако некоторые его части содержат очень поверхностные примеры кода и касаются только поверхности обсуждаемой проблемы из-за своего размера. По этой причине, чтобы увидеть более полные примеры кода, обратитесь к репозиторию Git моего примера проекта GraphQL API: graphql-example.

В конце хочу сказать, что GraphQL действительно интересная технология. Заменит ли он REST? Никто не знает, может завтра в быстро меняющемся мире IT появится какой-то более совершенный подход к разработке API, но GraphQL действительно попадает в разряд интересных технологий, которые точно стоит изучить.