REST? Возьмите тупой JSON-RPC +196


В последнее время на Хабре разгорелось много споров по поводу того, как правильно готовить REST API.

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

Возможно, именно вашему проекту RPC-like API подойдет лучше?

Итак, что такое JSON-RPC 2.0?
Это простой stateless-протокол для создания API в стиле RPC (Remote Procedure Call).
Выглядит это обычно следующим образом.

У вас на сервере есть один единственный endpoint, который принимает запросы с телом вида:

{"jsonrpc": "2.0", "method": "post.like", "params": {"post": "12345"}, "id": 1}

И отдает ответы вида:

{"jsonrpc": "2.0", "result": {"likes": 123}, "id": 1}

Если возникает ошибка — ответ об ошибке:

{"jsonrpc": "2.0", "error": {"code": 666, "message": "Post not found"}, "id": "1"}

И это всё!

Бонусом поддерживаются batch-операции:

Request:

[
  {"jsonrpc":"2.0","method":"server.shutdown","params":{"server":"42"},"id":1},
  {"jsonrpc":"2.0","method":"server.remove","params":{"server":"24"},"id":2}
]

Response:

[
  {"jsonrpc":"2.0","result":{"status":"down"},"id":1}
  {"jsonrpc":"2.0","error":{"code":1234,"message":"Server not found"},"id": 2}
]

В поле id клиент API может отправлять что угодно, дабы после получения ответов от сервера сопоставить их с запросами.

Также клиент может отправлять «нотификации» — запросы без поля «id», которые не требуют ответа от сервера:

{"jsonrpc":"2.0","method":"analytics:trackView","params":{"type": "post", "id":"123"}},

Библиотеки для клиента и сервера есть, наверное, под все популярные языки.
Если нет — не беда. Протокол настолько простой, что написать свою реализацию займет пару часов.

Работа с RPC-клиентом, который мне первым попался на npmjs.com, выглядит так:

client.request('add', [1, 1], function(err, response) {
  if (err) throw err;
  console.log(response.result); // 2
});

Профиты


Согласованность с бизнес-логикой проекта

Во-первых, можно не прятать сложные операции за скудным набором HTTP-глаголов и избыточными URI.

Есть предметные области, где операций в API должно быть больше чем сущностей.
Навскидку — проекты с непростыми бизнес-процессами, gamedev, мессенджеры и подобные realtime-штуки.

Да даже взять контентный проект вроде Хабра…

Нажатие кнопки "^" под постом — это не изменение ресурса, а вызов целой цепочки событий, вплоть до выдачи автору поста значков или инвайтов.

Так стоит ли маскировать post.like(id) за PUT /posts/{id}/likes?

Здесь также стоит упомянуть CQRS, с которым RPC-шный API будет смотреться лучше.

Во-вторых, кодов ответа в HTTP всегда меньше, чем типов ошибок бизнес-логики, которые вы бы хотели возвращать на клиент.

Кто-то всегда возвращает 200-ку, кто-то ломает голову, пытаясь сопоставить ошибки с HTTP-кодами.

В JSON-RPC весь диапазон integer — ваш.

JSON-RPC — стандарт, а не набор рекомендаций

Очень простой стандарт.
Данные запроса могут быть:
REST RPC
В URI запроса ---
В GET-параметрах ---
В HTTP-заголовках ---
В теле запроса В теле запроса

Данные ответа могут быть:
REST RPC
В HTTP-коде ответа ---
В HTTP-заголовках ---
В теле ответа (формат не стандартизирован) В теле ответа (формат стандартизирован)


POST /server/{id}/status или PATCH /server/{id}?
Это больше не имеет значения. Остается POST /api.

Нет никаких best practices с форумов, есть стандарт.
Нет разногласий в команде, есть стандарт.

Конечно же, качественно реализованный REST API можно полностью задокументировать. Однако…

Знаете, что и где нужно передать в запросе к Github API, чтобы получить объект reactions вместе с issue?
Accept: application/vnd.github.squirrel-girl-preview
Хорошо это или плохо? Решайте сами, гуглите сами. Стандарта нет.

Независимость от HTTP

В теории, принципы REST можно применять не только для API поверх HTTP.
На практике все по-другому.

JSON-RPC over HTTP безболезненно переносится на JSON-RPC over Websocket. Да хоть TCP.
Тело JSON-RPC запроса можно прямо в сыром виде бросить в очередь, чтобы обработать позже.

Больше нет проблем от размазывания бизнес-логики по транспортному уровню (HTTP).

HTTP 404
REST RPC
Ресурса с таким идентификатором нет ---
Здесь API нет Здесь API нет


Производительность

JSON-RPC пригодится, если у вас есть:
— Batch-запросы
— Нотификации, которые можно обрабатывать асинхронно
— Вебсокеты

Не то, чтобы это все нельзя было сделать без JSON-RPC. Но с ним — чуть легче.

Подводные камни


HTTP-кеширование

Если вы собираетесь кешировать ответы вашего API на уровне HTTP — RPC может не подойти.
Обычно это бывает, если у вас публичное, преимущественно read-only API.
Что-то вроде получения прогноза погоды или курса валют.

Если ваше API более «динамичное» и предназначено для «внутреннего» использования — все ок.

access.log

Все запросы к JSON-RPC API в логах веб-сервера выглядят одинаково.
Решается логированием на уровне приложения.

Документирование

Для JSON-RPC нет инструмента уровня swagger.io.
Подойдет apidocjs.com, но он гораздо скромнее.
Впрочем, документировать такой простой API можно хоть в markdown-файле.

Stateless

«REST»? — об архитектуре, а не глаголах? HTTP — возразите вы. И будете правы.

В оригинальной диссертации Роя Филдинга не указано, какие именно глаголы, заголовки и коды HTTP нужно использовать.

Зато в ней есть волшебное слово, которое пригодится даже при проектировании RPC API. «Stateless».
Каждый запрос клиента к серверу должен содержать всю информацию, необходимую для выполнения этого запроса, без хранения какого-либо контекста на стороне сервера. Состояние сеанса целиком хранится на стороне клиента.
Делая RPC API поверх веб-сокетов, может возникнуть соблазн заставить сервер приложения хранить чуть больше данных о сессии клиента, чем нужно.

Насколько stateless должен быть API, чтобы не причинять проблем? Для контраста вспомним по-настоящему statefull протокол? — ?FTP.

Клиент: [открывает TCP-соединение]
Сервер: 220 ProFTPD 1.3.1 Server (ProFTPD)
Клиент: USER anonymous
Сервер: 331 Anonymous login ok, send complete email address as your password
Клиент: PASS user@example.com
Сервер: 230 Anonymous access granted, restrictions apply
Клиент: CWD posts/latest
Сервер: 250 CWD command successful
Клиент: RETR rest_api.txt
Сервер: 150 Opening ASCII mode data connection for rest_api.txt (4321 bytes)
Сервер: 226 Transfer complete
Клиент: QUIT
Сервер: 221 Goodbye.


Состояние сеанса хранится на сервере. FTP-сервер помнит, что клиент уже прошел аутентификацию в начале сеанса, и помнит, в каком каталоге сейчас «находится» этот клиент.

Такой API сложно разрабатывать, дебажить и масштабировать. Не делайте так.

В итоге


Возьмите JSON-RPC 2.0, если решитесь сделать RPC API поверх HTTP или веб-сокетов.
Можете, конечно, придумать свой велосипед, но зачем?

Возьмите GraphQL, если он правда вам нужен.

Возьмите gRPC или что-то подобное для коммуникации между (микро)сервисами, если ваш ЯП это поддерживает.

Возьмите REST, если нужен именно он. Теперь вы, по крайней мере, выберете его осознанно.




К сожалению, не доступен сервер mySQL