GraphQL против REST — учебник по GraphQL

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

Возможно, вы слышали о новинке: GraphQL. Если нет, то GraphQL — это, одним словом, новый способ получения API, альтернатива REST. Он начинался как внутренний проект Facebook, и, поскольку он был с открытым исходным кодом, он приобрел большую популярность.

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

GraphQL против REST — учебник по GraphQL

Первая часть статьи начнется с трех причин, по которым я лично считаю, что GraphQL лучше REST. Вторая часть — это руководство о том, как добавить конечную точку GraphQL в серверную часть.

Graphql против REST: зачем отказываться от REST?

Если вы все еще сомневаетесь, подходит ли GraphQL для ваших нужд, здесь приведен довольно обширный и объективный обзор «REST против GraphQL». Тем не менее, чтобы узнать о трех главных причинах использования GraphQL, читайте дальше.

Причина 1: производительность сети

Скажем, у вас есть пользовательский ресурс на сервере с именем, фамилией, адресом электронной почты и 10 другими полями. На клиенте вам обычно нужна только пара из них.

Выполнение вызова REST в конечной точке /users возвращает вам все поля пользователя, и клиент использует только те, которые ему нужны. Очевидно, что есть некоторые потери при передаче данных, которые могут быть рассмотрены в мобильных клиентах.

GraphQL по умолчанию извлекает наименьшие возможные данные. Если вам нужны только имена и фамилии ваших пользователей, вы указываете это в своем запросе.

Приведенный ниже интерфейс называется GraphiQL, он похож на обозреватель API для GraphQL. Я создал небольшой проект для целей этой статьи. Код размещен на GitHub, и мы углубимся в него во второй части.

На левой панели интерфейса находится запрос. Здесь мы извлекаем всех пользователей — мы бы сделали GET /users с REST — и получаем только их имена и фамилии.

Запрос

 query { users { firstname lastname } }

Результат

 { "data": { "users": [ { "firstname": "John", "lastname": "Doe" }, { "firstname": "Alicia", "lastname": "Smith" } ] } }

Если бы мы также хотели получать электронные письма, добавление строки «email» после «lastname» помогло бы.

Некоторые серверные части REST предлагают такие параметры, как /users?fields=firstname,lastname для частичного возврата ресурсов. Как бы то ни было, Google рекомендует это. Однако он не реализован по умолчанию и делает запрос едва читаемым, особенно если добавить другие параметры запроса:

  • &status=active для фильтрации активных пользователей
  • &sort=createdAat для сортировки пользователей по дате их создания
  • &sortDirection=desc , потому что вам это явно нужно
  • &include=projects для включения проектов пользователей

Эти параметры запроса являются исправлениями, добавленными в REST API для имитации языка запросов. GraphQL — это, прежде всего, язык запросов, который с самого начала делает запросы краткими и точными.

Причина 2: выбор дизайна «включить или конечную точку»

Давайте представим, что мы хотим создать простой инструмент управления проектами. У нас есть три ресурса: пользователи, проекты и задачи. Мы также определяем следующие отношения между ресурсами:

Отношения между ресурсами

Вот некоторые из конечных точек, которые мы представляем миру:

Конечная точка Описание
GET /users Список всех пользователей
GET /users/:id Получить одного пользователя с идентификатором: id
GET /users/:id/projects Получить все проекты одного пользователя

Конечные точки просты, легко читаемы и хорошо организованы.

Все становится сложнее, когда наши запросы становятся более сложными. Возьмем конечную точку GET /users/:id/projects : скажем, я хочу показывать только названия проектов на домашней странице, но проекты+задачи на панели инструментов, без выполнения нескольких вызовов REST. Я бы позвонил:

  • GET /users/:id/projects для домашней страницы.
  • GET /users/:id/projects?include=tasks (например) на странице панели инструментов, чтобы серверная часть добавляла все связанные задачи.

Обычной практикой является добавление параметров запроса ?include=... , чтобы это работало, и даже рекомендуется спецификацией JSON API. Параметры запроса, такие как ?include=tasks , по-прежнему доступны для чтения, но вскоре мы получим ?include=tasks,tasks.owner,tasks.comments,tasks.comments.author .

В этом случае не будет ли разумнее создать для этого конечную точку /projects ? Что-то вроде /projects?userId=:id&include=tasks , так как нам нужно включить на один уровень отношений меньше? Или, на самом деле, конечная точка /tasks?userId=:id тоже может работать. Это может быть трудным выбором дизайна, еще более сложным, если у нас есть отношение «многие ко многим».

GraphQL везде использует подход include . Это делает синтаксис для извлечения отношений мощным и согласованным.

Вот пример получения всех проектов и задач от пользователя с идентификатором 1.

Запрос

 { user(id: 1) { projects { name tasks { description } } } }

Результат

 { "data": { "user": { "projects": [ { "name": "Migrate from REST to GraphQL", "tasks": [ { "description": "Read tutorial" }, { "description": "Start coding" } ] }, { "name": "Create a blog", "tasks": [ { "description": "Write draft of article" }, { "description": "Set up blog platform" } ] } ] } } }

Как видите, синтаксис запроса легко читается. Если бы мы хотели пойти глубже и включить задачи, комментарии, изображения и авторов, мы бы не задумывались над тем, как организовать наш API. GraphQL упрощает получение сложных объектов.

Причина 3: управление разными типами клиентов

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

С REST есть несколько решений. Мы можем создать пользовательскую конечную точку (т. е. псевдоним конечной точки, например, /mobile_user ), пользовательское представление ( Content-Type: application/vnd.rest-app-example.com+v1+mobile+json ) или даже клиент. -специфический API (как когда-то это делал Netflix). Все три из них требуют дополнительных усилий от команды разработчиков.

GraphQL дает больше возможностей клиенту. Если клиенту нужны сложные запросы, он сам построит соответствующие запросы. Каждый клиент может использовать один и тот же API по-разному.

Как начать с GraphQL

В большинстве сегодняшних дебатов о «GraphQL против REST» люди думают, что они должны выбрать одно из двух. Это просто неправда.

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

Сравнение конечных точек GraphQL и REST

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

Как было сказано ранее, я проиллюстрирую это руководство небольшим примером проекта, доступным на GitHub. Это упрощенный инструмент управления проектами с пользователями, проектами и задачами.

В этом проекте используются следующие технологии: Node.js и Express для веб-сервера, SQLite в качестве реляционной базы данных и Sequelize в качестве ORM. Три модели — пользовательская, проектная и задача — определены в папке models . Конечные точки REST /api/users , /api/projects и /api/tasks доступны для всего мира и определены в папке rest .

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

Наша цель — создать конечную точку /graphql , не удаляя конечные точки REST. Конечная точка GraphQL напрямую обращается к ORM базы данных для извлечения данных, поэтому она полностью независима от логики REST.

Типы

Модель данных представлена ​​в GraphQL типами , которые строго типизированы. Между вашими моделями и типами GraphQL должно быть сопоставление 1-к-1. Наш тип User будет:

 type User { id: ID! # The "!" means required firstname: String lastname: String email: String projects: [Project] # Project is another GraphQL type }

Запросы

Запросы определяют, какие запросы вы можете выполнять в API GraphQL. По соглашению должен быть RootQuery , содержащий все существующие запросы. Я также указал REST-эквивалент каждого запроса:

 type RootQuery { user(id: ID): User # Corresponds to GET /api/users/:id users: [User] # Corresponds to GET /api/users project(id: ID!): Project # Corresponds to GET /api/projects/:id projects: [Project] # Corresponds to GET /api/projects task(id: ID!): Task # Corresponds to GET /api/tasks/:id tasks: [Task] # Corresponds to GET /api/tasks }

Мутации

Если запросы являются запросами GET , мутации можно рассматривать как запросы POST / PATCH / PUT / DELETE (хотя на самом деле это синхронизированные версии запросов).

По соглашению мы помещаем все наши мутации в RootMutation :

 type RootMutation { createUser(input: UserInput!): User # Corresponds to POST /api/users updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users removeUser(id: ID!): User # Corresponds to DELETE /api/users createProject(input: ProjectInput!): Project updateProject(id: ID!, input: ProjectInput!): Project removeProject(id: ID!): Project createTask(input: TaskInput!): Task updateTask(id: ID!, input: TaskInput!): Task removeTask(id: ID!): Task }

Обратите внимание, что здесь мы представили новые типы, называемые UserInput , ProjectInput и TaskInput . Это обычная практика и для REST, чтобы создать модель входных данных для создания и обновления ресурсов. Здесь наш тип UserInput — это наш тип User без полей id и projects , и обратите внимание на input ключевого слова вместо type :

 input UserInput { firstname: String lastname: String email: String }

Схема

С помощью типов, запросов и мутаций мы определяем схему GraphQL , которую конечная точка GraphQL предоставляет миру:

 schema { query: RootQuery mutation: RootMutation }

Эта схема строго типизирована, и именно она позволила нам использовать эти удобные автозаполнения в GraphiQL.

Резольверы

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

  • Достичь внутренней конечной точки REST
  • Вызов микросервиса
  • Перейдите на уровень базы данных, чтобы выполнять операции CRUD.

Мы выбираем третий вариант в нашем примере приложения. Давайте посмотрим на наш файл резольверов:

 const models = sequelize.models; RootQuery: { user (root, { id }, context) { return models.User.findById(id, context); }, users (root, args, context) { return models.User.findAll({}, context); }, // Resolvers for Project and Task go here }, /* For reminder, our RootQuery type was: type RootQuery { user(id: ID): User users: [User] # Other queries }

Это означает, что если запрос user(id: ID!) запрашивается в GraphQL, то мы возвращаем User.findById() , который является функцией Sequelize ORM, из базы данных.

Как насчет того, чтобы присоединиться к другим моделям в запросе? Что ж, нам нужно определить больше распознавателей:

 User: { projects (user) { return user.getProjects(); // getProjects is a function managed by Sequelize ORM } }, /* For reminder, our User type was: type User { projects: [Project] # We defined a resolver above for this field # ...other fields } */

Поэтому, когда мы запрашиваем поле projects в типе User в GraphQL, это соединение будет добавлено к запросу базы данных.

И, наконец, резолверы для мутаций:

 RootMutation: { createUser (root, { input }, context) { return models.User.create(input, context); }, updateUser (root, { id, input }, context) { return models.User.update(input, { ...context, where: { id } }); }, removeUser (root, { id }, context) { return models.User.destroy(input, { ...context, where: { id } }); }, // ... Resolvers for Project and Task go here }

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

Запрос

 query getUserWithProjects { user(id: 2) { firstname lastname projects { name tasks { description } } } } mutation createProject { createProject(input: {name: "New Project", UserId: 2}) { id name } }

Результат

 { "data": { "user": { "firstname": "Alicia", "lastname": "Smith", "projects": [ { "name": "Email Marketing Campaign", "tasks": [ { "description": "Get list of users" }, { "description": "Write email template" } ] }, { "name": "Hire new developer", "tasks": [ { "description": "Find candidates" }, { "description": "Prepare interview" } ] } ] } } }

Переписывание всех типов, запросов и преобразователей для существующего приложения может занять некоторое время. Тем не менее, существует множество инструментов, которые помогут вам. Например, есть инструменты, которые переводят схему SQL в схему GraphQL, включая преобразователи!

Собираем все вместе

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

 // Mount GraphQL on /graphql const schema = makeExecutableSchema({ typeDefs, // Our RootQuery and RootMutation schema resolvers: resolvers() // Our resolvers }); app.use('/graphql', graphqlExpress({ schema }));

И у нас может быть красивый интерфейс GraphiQL на нашем сервере. Чтобы сделать запрос без GraphiQL, просто скопируйте URL-адрес запроса и запустите его с помощью cURL, AJAX или непосредственно в браузере. Конечно, есть несколько клиентов GraphQL, которые помогут вам создавать эти запросы. Ниже приведены некоторые примеры.

Что дальше?

Цель этой статьи — дать вам представление о том, как выглядит GraphQL, и показать, что определенно можно попробовать GraphQL, не отказываясь от инфраструктуры REST. Лучший способ узнать, соответствует ли GraphQL вашим потребностям, — попробовать его самостоятельно. Я надеюсь, что эта статья заставит вас погрузиться.

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

Ниже приведены некоторые другие полезные ресурсы:

Серверный инструмент Описание
graphql-js Эталонная реализация GraphQL. Вы можете использовать его с express-graphql для создания сервера.
graphql-server Универсальный сервер GraphQL, созданный командой Apollo.
Реализации для других платформ Руби, PHP и т.д.
Клиентский инструмент Описание
Реле Фреймворк для соединения React с GraphQL.
аполлон-клиент. Клиент GraphQL с привязками для React, Angular 2 и других интерфейсных фреймворков.

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