В последнее время на Хабре разгорелось много споров по поводу того, как правильно готовить 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, если нужен именно он. Теперь вы, по крайней мере, выберете его осознанно.
Автор: Roman Vasilyev