Два способа отправки данных через HTTP. В чем разница?

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

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

Итак, приступим. Мы определим некоторые свойства API, а затем обсудим, как GraphQL и REST обрабатывают их.

Ресурсы

Основная идея REST - это ресурс. Каждый ресурс идентифицируется URL-адресом, и вы получаете этот ресурс, отправляя GET запрос на этот URL-адрес. Скорее всего, вы получите ответ в формате JSON, так как это то, что в наши дни использует большинство API. Это выглядит примерно так:

GET /books/1
{
  "title": "Black Hole Blues",
  "author": { 
    "firstName": "Janna",
    "lastName": "Levin"
  }
  // ... more fields here
}

Примечание. В приведенном выше примере некоторые API-интерфейсы REST возвращают «author» как отдельный ресурс.

В REST следует отметить, что тип или форма ресурса и способ его получения связаны. Когда вы говорите об этом в документации REST, вы можете называть это «конечной точкой книги».

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

type Book {
  id: ID
  title: String
  published: Date
  price: String
  author: Author
}
type Author {
  id: ID
  firstName: String
  lastName: String
  books: [Book]
}

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

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

type Query {
  book(id: ID!): Book
  author(id: ID!): Author
}

Теперь мы можем отправить запрос, аналогичный REST-запросу выше, но на этот раз с GraphQL:

GET /graphql?query={ book(id: "1") { title, author { firstName } } }
{
  "title": "Black Hole Blues",
  "author": {
    "firstName": "Janna",
  }
}

Отлично, теперь мы к чему-то приближаемся! Мы сразу можем увидеть несколько вещей в GraphQL, которые сильно отличаются от REST, хотя оба могут быть запрошены через URL-адрес, и оба могут возвращать одинаковую форму ответа JSON.

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

Но самое главное, идентичность ресурсов, концепции Книг и Авторов не связаны с тем, как они были получены. Мы могли бы получить одну и ту же Книгу с помощью множества различных типов запросов и с разными наборами полей.

Заключение

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

  • Аналогично: оба имеют представление о ресурсе и могут указывать идентификаторы для этих ресурсов.
  • Аналогично: оба могут быть получены с помощью HTTP-запроса GET с URL-адресом.
  • Аналогично: оба могут возвращать данные JSON в запросе.
  • Другое. В REST вызываемая конечная точка является идентификатором этого объекта. В GraphQL идентификация не зависит от способа ее получения.
  • Другой: В REST форма и размер ресурса определяется сервером. В GraphQL сервер объявляет, какие ресурсы доступны, а клиент спрашивает, что ему нужно в данный момент.

Хорошо, это было довольно просто, если вы уже использовали GraphQL и / или REST. Если вы раньше не использовали GraphQL, вы можете поиграть с примером, подобным приведенному выше на Launchpad, инструменте для создания и изучения примеров GraphQL в вашем браузере.

URL-маршруты против схемы GraphQL

API бесполезен, если он непредсказуем. Когда вы используете API, вы обычно делаете это как часть некоторой программы, и эта программа должна знать, что она может вызывать и что она должна ожидать в результате, чтобы она могла работать с этим результатом.

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

В современных REST API этот API обычно описывается как список конечных точек:

GET /books/:id
GET /authors/:id
GET /books/:id/comments
POST /books/:id/comments

Таким образом, можно сказать, что «форма» API линейна - есть список вещей, к которым вы можете получить доступ. Когда вы извлекаете данные или что-то сохраняете, первый вопрос, который нужно задать: «К какой конечной точке мне следует позвонить»?

В GraphQL, как мы уже говорили выше, вы не используете URL-адреса для определения того, что доступно в API. Вместо этого вы используете схему GraphQL:

type Query {
  book(id: ID!): Book
  author(id: ID!): Author
}
type Mutation {
  addComment(input: AddCommentInput): Comment
}
type Book { ... }
type Author { ... }
type Comment { ... }
input AddCommentInput { ... }

Здесь есть несколько интересных моментов по сравнению с маршрутами REST для аналогичного набора данных. Во-первых, вместо того, чтобы отправлять разные HTTP-команды по одному и тому же URL-адресу, чтобы различать чтение и запись, GraphQL использует другой начальный тип - Мутация против запроса. В документе GraphQL вы можете выбрать, какой тип операции вы отправляете с ключевым словом:

query { ... }
mutation { ... }

Все подробности о языке запросов читайте в моей предыдущей публикации Анатомия запроса GraphQL.

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

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

Заключение

В REST пространство доступных данных описывается как линейный список конечных точек, а в GraphQL это схема со связями.

  • Аналогично: список конечных точек в REST API аналогичен списку полей типов Query и Mutation в GraphQL API. Они оба являются точками входа в данные.
  • Аналогично: у обоих есть способ различать, предназначен ли запрос API для чтения данных или их записи.
  • Другой: В GraphQL вы можете перейти от точки входа к связанным данным, следуя отношениям, определенным в схеме, за один запрос. В REST вам нужно вызвать несколько конечных точек для получения связанных ресурсов.
  • Другое: в GraphQL нет разницы между полями типа Query и полями любого другого типа, за исключением того, что в корне запроса доступен только тип запроса. Например, у вас могут быть аргументы в любом поле запроса. В REST нет первоклассной концепции вложенного URL.
  • Другой: В REST вы указываете запись, изменяя HTTP-команду с GET на что-то другое, например POST. В GraphQL вы меняете ключевое слово в запросе.

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

Обработчики маршрутов против преобразователей

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

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

Давайте посмотрим на пример hello world с помощью express, популярной библиотеки API для Node:

app.get('/hello', function (req, res) {
  res.send('Hello World!')
})

Здесь вы видите, что мы создали /hello конечную точку, которая возвращает строку 'Hello World!'. В этом примере мы можем увидеть жизненный цикл HTTP-запроса на сервере REST API:

  1. Сервер получает запрос и извлекает команду HTTP (в данном случае GET) и путь URL.
  2. Библиотека API сопоставляет глагол и путь к функции, зарегистрированной серверным кодом.
  3. Функция выполняется один раз и возвращает результат
  4. Библиотека API сериализует результат, добавляет соответствующий код ответа и заголовки и отправляет его обратно клиенту.

GraphQL работает очень похоже, и для того же hello world example он практически идентичен:

const resolvers = {
  Query: {
    hello: () => {
      return 'Hello world!';
    },
  },
};

Как видите, вместо предоставления функции для определенного URL-адреса мы предоставляем функцию, которая соответствует определенному полю в типе, в данном случае поле hello в типе Query. В GraphQL эта функция, реализующая поле, называется преобразователем.

Чтобы сделать запрос, нам понадобится запрос:

query {
  hello
}

Итак, вот что происходит, когда наш сервер получает запрос GraphQL:

  1. Сервер получает запрос и получает запрос GraphQL.
  2. Запрос просматривается, и для каждого поля вызывается соответствующий преобразователь. В данном случае есть только одно поле, hello, и оно относится к типу Query
  3. Функция вызывается, и она возвращает результат
  4. Библиотека GraphQL и сервер прикрепляют этот результат к ответу, который соответствует форме запроса.

Так ты вернешься:

{ "hello": "Hello, world!" }

Но вот одна уловка: мы можем вызвать поле дважды!

query {
  hello
  secondHello: hello
}

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

Это было бы неполным без примера «вложенных» преобразователей:

{
  Query: {
    author: (root, { id }) => find(authors, { id: id }),
  },
  Author: {
    posts: (author) => filter(posts, { authorId: author.id }),
  },
}

Эти преобразователи смогут выполнить такой запрос:

query {
  author(id: 1) {
    firstName
    posts {
      title
    }
  }
}

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

Посмотрите полный пример и запустите разные запросы, чтобы проверить это!

Заключение

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

  • Аналогично: конечные точки в REST и поля в GraphQL в конечном итоге вызывают функции на сервере.
  • Аналогично. И REST, и GraphQL обычно полагаются на фреймворки и библиотеки для обработки мельчайших деталей сетевого шаблона.
  • Разное. В REST каждый запрос обычно вызывает ровно одну функцию обработчика маршрута. В GraphQL один запрос может вызывать множество преобразователей для создания вложенного ответа с несколькими ресурсами.
  • Другой. В REST вы сами создаете форму ответа. В GraphQL форма ответа создается библиотекой выполнения GraphQL, чтобы соответствовать форме запроса.

По сути, вы можете рассматривать GraphQL как систему для вызова множества вложенных конечных точек в одном запросе. Почти как мультиплексированный REST.

Что все это значит?

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

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

С другой стороны, GraphQL пока не имеет такого количества инструментов и интеграций, как REST. Например, вы не можете кэшировать результаты GraphQL с помощью HTTP-кеширования так же легко, как результаты REST. Но сообщество упорно работает над улучшением инструментов и инфраструктуры. Например, вы можете кэшировать результаты GraphQL в своем интерфейсе с помощью Apollo Client и Relay, а в последнее время также на сервере с Apollo Engine.

Есть еще идеи о сравнении REST и GraphQL? Пожалуйста, разместите их в комментариях!