Skip to content

Latest commit

 

History

History
509 lines (413 loc) · 22.5 KB

File metadata and controls

509 lines (413 loc) · 22.5 KB

API: REST, GraphQL, RPC и gRPC

Заметка о способах, которыми программы запрашивают друг у друга данные и действия: ресурсы в REST, граф в GraphQL, удалённые процедуры в RPC и gRPC. Про сам протокол HTTP, поверх которого работает большинство из них, — в заметке про протоколы.

REST

REST (англ. Representational State Transfer) — архитектурный стиль, описанный Роем Филдингом в его диссертации (Fielding, глава 5). Это не протокол и не формат, а набор ограничений: система, которая им следует, называется RESTful.

Ограничения REST

Филдинг собирает стиль из последовательности ограничений:

  • Клиент-сервер — интерфейс отделён от хранения данных.
  • Отсутствие состояния (англ. stateless) — каждый запрос клиента должен содержать всю информацию, нужную для его понимания: сервер не хранит контекст между запросами.
  • Кэширование — ответ должен быть явно или неявно помечен как кэшируемый или некэшируемый.
  • Единообразный интерфейс (англ. uniform interface) — центральная черта, которая отличает REST от других сетевых стилей.
  • Многоуровневая система — клиент не знает, говорит ли он с сервером напрямую или через посредников.
  • Код по требованию — необязательное ограничение: сервер может передать клиенту исполняемый код.

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

Ресурсы и методы HTTP

В REST всё строится вокруг ресурсов — сущностей, у которых есть адрес: /users, /users/17, /users/17/orders. Клиент получает не сам ресурс, а его представление — обычно JSON.

Действие над ресурсом задаётся методом HTTP. Спецификация HTTP делит методы по двум свойствам (RFC 9110): метод безопасный, если его смысл — по сути только чтение; метод идемпотентный, если несколько одинаковых запросов имеют тот же задуманный эффект, что и один.

Метод Что делает с ресурсом Безопасный Идемпотентный
GET получить да да
POST создать или выполнить действие нет нет
PUT заменить целиком нет да
PATCH изменить частично нет нет
DELETE удалить нет да

Безопасность и идемпотентность GET, PUT, DELETE — по RFC 9110, PATCH не является ни безопасным, ни идемпотентным по RFC 5789 (RFC 5789).

Эти свойства — не формальность. Идемпотентный запрос можно безопасно повторить при обрыве связи; безопасный — закэшировать и отдать из кэша. На этом и держится главное практическое преимущество REST — кэширование средствами самого HTTP.

REST на практике

Данные в QUERY

  • Форма запроса на клиенте.
GET /route?field=value&anotherField=anotherValue
/* axios */
axios.get('/route', {
  params: {
    field: 'value',
    anotherField: 'anotherValue',
  },
});
  • Форма запроса на сервере.
GET /route
  • Получение данных из запроса на сервере.
/* Express */
app.get('/route', (request, response) => {
  const { field, anotherField } = request.query;
});

Передача массива arr [1, 3, 7] с клиента.

GET /route?arr[]=1&arr[]=3&arr[]=7

Передача объекта obj { foo: '1', bar: '7' } с клиента.

GET /route?obj[foo]=1&obj[bar]=7

Важно: запись с квадратными скобками — не стандарт HTTP, а соглашение библиотеки qs, которая умеет разбирать вложенные объекты и массивы, указанные через [] (qs). В Express 5 парсер строки запроса по умолчанию сменили с расширенного (extended) на простой (simple) (руководство по переходу на Express 5) — поэтому вложенные объекты так по умолчанию больше не разбираются: расширенный разбор нужно включать явно настройкой query parser.

Данные в BODY

  • Форма запроса на клиенте.
POST /route
Content-Type: application/json

{ "field": "value", "anotherField": "anotherValue" }
/* axios */
axios.post('/route', {
  field: 'value',
  anotherField: 'anotherValue',
});
  • Форма запроса на сервере.
POST /route
  • Получение данных из запроса на сервере.
/* Express */
app.post('/route', (request, response) => {
  const { field, anotherField } = request.body;
});

Данные в PARAMS

  • Форма запроса на клиенте.
GET /route/paramValue
/* axios */
axios.get(`/route/${paramValue}`);
  • Форма запроса на сервере.
GET /route/:param
  • Получение данных из запроса на сервере.
/* Express */
app.get('/route/:param', (request, response) => {
  const { param } = request.params;
});

Пример отправки нескольких параметров.

PATCH /users/17/name
PATCH /users/:id/:field

GraphQL

GraphQL — язык запросов (query language) для API.

Клиент посылает запрос (query) к сервису GraphQL и получает ответ в виде JSON-объекта по указанной в запросе схеме.

Например, клиент может послать запрос.

query {
  currentUser {
    id
    username
    role
  }
}

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

{
  "data": {
    "currentUser": {
      "id": "1",
      "username": "Notes",
      "role": "Admin"
    }
  }
}

Операции query, mutation и subscription

Спецификация выделяет три типа операций (спецификация GraphQL): query — чтение, mutation — запись, за которой следует чтение, и subscription — долгоживущий запрос, который получает данные в ответ на события.

Запрос query отвечает за получение данных, он не может их изменять (аналог GET-запроса в REST).

query {
  users {
    id
    username
  }
}

Запрос mutation отвечает за изменение данных (объединяет в себе возможности POST, PUT, DELETE и других в REST).

mutation {
  logOut
}

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

subscription {
  messageAdded {
    id
    text
  }
}

Схемы и типы

Все поля, которые может получить клиент, должны быть описаны в типе (type).

Типы Query и Mutation являются типами по умолчанию. В них должны быть описаны все возможные запросы.

type Query {
  currentUser: User
}

type Mutation {
  logOut: Boolean
}
schema {
  query: Query
  mutation: Mutation
}

Могут создаваться пользовательские типы.

type User {
  id: ID
  username: String
  role: String
  image: String
}

Если схема в запросе с клиента не удовлетворяет схеме на сервере, то возникнет валидационная ошибка. Например, поле age отсутствует в типе User.

{
  "data": null,
  "errors": [
    {
      "...": "...",
      "message": "Validation error of type FieldUndefined: Field 'age' in type 'User' is undefined"
    }
  ]
}

Динамические объекты в GraphQL

GraphQL не позволяет создавать динамические объекты в качестве type.

Примеры динамических объектов.

const commentsMap = {
  123: {
    id: "123",
    message: "Hello"
  },
  456: {
    id: "456",
    message: "Hi"
  }
};
const reportsInfoMap = {
  spam: ["id1", "id2"],
  flood: [],
  bullying: ["id3"]
}
const flags = {
  isReviewed: true,
  isVisited: true,
  /* ... */
}

Есть два решения, как это можно обойти

  • Передавать объекты текстовый как JSON и указывать встроенный тип String (не лучшая валидация) или подключить библиотеку, которая имеет скалярный тип JSON (например, эту). Например, AWS AppSync имеет встроенный тип AWSJSON.
type Res {
  commentsMap: String
  reportsMap: JSON
  flags: AWSJSON
}
  • Представить объекты в виде массивов (предпочтительный вариант).
type Comment {
  id: ID
  message: String
}

type ReportsInfo {
  name: String
  values: [ID]
}

type Flag {
  name: String
  value: Boolean
}

type Res {
  commentsMap: [Comment]
  reportsMap: [ReportsInfo]
  flags: [Flag]
}

Взаимодействие с сервером

Отправка query

query Users {
  users {
    id
    username
    role
  }
}

Query с переменными

query User($userId: ID!) {
  user(userId: $userId) {
    id
    username
    role
  }
}

Variables

{
  "userId": "auth0|72d45e398924235638341891"
}

Mutation с input

Вход в систему меняет состояние — создаёт сессию, — поэтому это mutation, а не query.

mutation Login($credentialsInput: Credentials) {
  login(credentials: $credentialsInput) {
    auth_token
  }
}

Variables

{
  "credentialsInput": {
    "username": "admin",
    "password": "admin"
  }
}

Преимущества GraphQL

  • Строгая типизация. Конкретная схема, полностью описывающая, как можно работать с данными.
type User {
  id: ID!
  username: String!
}

type Article {
  id: ID!
  title: String!
  description: String
  author: User!
}

type Query {
  articles: [Article]
  article(id: ID!): Article
}
  • Клиент всегда запрашивает только то, что ему нужно. Он не может получить те поля, которые не запрашивал, ровно как и те, которые не предусмотрены описанной схемой.

Следующий запрос вернёт articles, содержащие только поля id и title. Все эти поля должны присутствовать в схеме запроса на бэкенде (описана выше), иначе будет ошибка.

query {
  articles {
    id
    title
  }
}
  • Один запрос может объединять в себе несколько различных ресурсов.

Следующий запрос объединяет в себе информацию, собранную из Articles и Users.

query {
  articles {
    id
    title
    description
    author {
      id
      username
    }
  }
}

GraphQL поверх HTTP

GraphQL не привязан к HTTP, но обычно работает поверх него, через один адрес. По руководству на graphql.org, сервер принимает POST для query и mutation и может принимать GET для query (graphql.org). Отсюда важное следствие для кэширования — см. сравнение.

RPC

Удалённым вызовом процедур (англ. Remote Procedure Call, RPC) называют подход, при котором программа вызывает функцию на другой машине так же, как вызывала бы локальную. В статье 1984 года, с которой началось широкое применение RPC, он описан как удобная парадигма связи между программами по сети (Birrell, Nelson, Implementing Remote Procedure Calls).

Главное отличие от REST — в том, что называется. В REST адрес называет ресурс (существительное): POST /users. В RPC вызов называет действие (глагол): createUser(...).

Самый простой пример — JSON-RPC 2.0: протокол удалённого вызова процедур без состояния (спецификация JSON-RPC). Он не зависит от транспорта: по спецификации, его можно использовать внутри одного процесса, через сокеты, поверх HTTP или в любой другой среде обмена сообщениями.

{ "jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1 }
{ "jsonrpc": "2.0", "result": 19, "id": 1 }

Поле id связывает ответ с запросом. Запрос без id называется уведомлением: клиент не ждёт на него ответа.

У удобства RPC есть обратная сторона: сетевой вызов только выглядит как локальный. Он может не дойти, зависнуть или выполниться дважды — поэтому таймауты и идемпотентность нужны здесь не меньше, чем в любом сетевом взаимодействии.

gRPC

gRPC — фреймворк удалённого вызова процедур, в котором клиент может напрямую вызвать метод серверного приложения на другой машине так, будто это локальный объект (документация gRPC).

Две вещи отличают его от «RPC вообще».

Первая — контракт. Сервис описывается заранее в файле .proto на языке Protocol Buffers — независимом от языка и платформы механизме сериализации структурированных данных (документация Protocol Buffers). По этому описанию генерируется код и клиента, и сервера — на любом из поддерживаемых языков.

syntax = "proto3";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

Сообщения передаются не текстом, а в двоичном виде: числа после полей — это номера, по которым поля кодируются, а не значения.

Вторая — транспорт и потоки. gRPC работает поверх кадров HTTP/2 (протокол gRPC поверх HTTP/2) и поддерживает четыре вида методов (документация gRPC):

Вид Как устроен В .proto
Унарный один запрос — один ответ, как обычный вызов функции rpc Get (Req) returns (Res);
Серверный поток один запрос — поток ответов rpc List (Req) returns (stream Res);
Клиентский поток поток запросов — один ответ rpc Upload (stream Req) returns (Res);
Двунаправленный обе стороны шлют последовательности сообщений rpc Chat (stream Req) returns (stream Res);

Ограничение для веба: браузер не может обратиться к gRPC-сервису напрямую. Для этого есть gRPC-Web, и его клиенты подключаются к сервисам через специальный прокси — по умолчанию это Envoy (gRPC-Web).

Сравнение

Признак REST GraphQL JSON-RPC gRPC
Что называет запрос ресурс нужные поля графа процедуру процедуру
Транспорт HTTP обычно HTTP, один адрес любой HTTP/2
Формат любой, обычно JSON JSON JSON Protocol Buffers, двоичный
Контракт не обязателен схема обязательна нет .proto обязателен
Кэширование средствами HTTP естественное для GET затруднено: запросы обычно POST нет нет
Поток от сервера вне модели подписки нет четыре вида методов
Из браузера да да да через gRPC-Web и прокси

Грубые ориентиры:

  • REST — публичные API, простые операции над сущностями, всё, что выигрывает от кэширования и понятности.
  • GraphQL — много разных клиентов с разными потребностями в данных: каждый берёт ровно то, что ему нужно, одним запросом.
  • JSON-RPC — небольшой набор действий, где ресурсная модель была бы натяжкой.
  • gRPC — взаимодействие сервисов между собой: строгий контракт, компактный двоичный формат и потоки.

Эти подходы не исключают друг друга. Частая схема: сервисы внутри системы общаются по gRPC, а наружу смотрит REST или GraphQL — потому что их понимает браузер и любой клиент без генерации кода.