REST и RPC для HTTP API
Последнее время при разработке HTTP API в основном используют REST,а не XML-RPC, SOAP и JSON-RPC. REST во многом превосходит другие «базирующиеся на RPC» подходы, которые могут ввести в заблуждение, из-за своих различий.
В данной статье рассматриваются REST и RPC в контексте создания HTTP API-интерфейсов, потому что именно они наиболее часто используются.
REST расшифровывается как «representational state transfer» (передача состояния представления), как описывает Рой Филдинг в своей диссертации. К сожалению, те кто не видел эту диссертацию, имеют свое представление о том, что такое REST и это приводит к большому беспорядку и разногласиям. REST позволяет настроить отношения клиент-сервер, где на стороне сервера данные предоставляются в простых форматах, часто JSON и XML. Такое представление ресурсов или коллекции ресурсов, которые затем могут изменяться клиентом, с помощью действий прописанных в гипермедиа на сервере. Гипермедиа имеет основополагающее значение для REST, по существу это только концепция предоставления ссылок на другие ресурсы.
Гипермедиа накладывает такие ограничения:
- Отсутствие состояния: не сохраняющиеся сессии между запросами. Понятие «без состояния» не означает, что серверы и клиенты его не имеют, у них просто нет необходимости отслеживать состояние друг друга. Когда клиент не взаимодействует с сервером, сервер не имеет представления о его существовании.
- Ответы должны объявлять кэшируемость: помогает вашей шкале API клиентов соблюдать правила.
- REST фокусируется на однородности: если вы используете HTTP, вы должны использовать функции HTTP везде где это возможно.
Эти ограничения (плюс еще несколько), использованые в REST архитектуре, помогают API в течение десятилетий, не только годов.
Перед тем как REST стал популярным (после того, как компании, такие как Twitter и Facebook, начали использовать его для своих API-интерфейсов), большинство API, были построены с использованием XML-RPC или SOAP.
«RPC» означает “remote procedure call» (удаленный вызов процедуры), и это в общем так же как вызов функции в JavaScript, PHP, Python и так далее, принимая имя и аргументы метода. Так как XML не каждому по вкусу, RPC API может использовать протокол JSON-RPC.
Возьмем такой пример RPC вызова:
POST /sayHello HTTP/1.1 HOST: api.example.com Content-Type: application/json {"name": "Racey McRacerson"}
В JavaScript мы будем делать то же самое. Определим функцию, а затем вызовем её в другом месте:
/* Signature */ function sayHello(name) { // ... } /* Usage */ sayHello("Racey McRacerson");
Идея такая же. API строится путем определения публичных методов, затем эти методы вызываются с аргументами. RPC это просто набор функций, но в контексте HTTP API, что предполагает ввод метода в URL и аргументы в строку запроса или тело. SOAP может быть невероятно многословным как для доступа к аналогичным данным, так и к отчетности. Если вы будете искать «пример SOAP» на Google, вы найдете пример с Google, который демонстрирует метод, названный getAdUnitsByStatement, который выглядит следующим образом:
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Header>
<ns1:RequestHeader
soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next"
soapenv:mustUnderstand="0"
xmlns:ns1="https://www.google.com/apis/ads/publisher/v201605">
<ns1:networkCode>123456</ns1:networkCode>
<ns1:applicationName>DfpApi-Java-2.1.0-dfp_test</ns1:applicationName>
</ns1:RequestHeader>
</soapenv:Header>
<soapenv:Body>
<getAdUnitsByStatement xmlns="https://www.google.com/apis/ads/publisher/v201605">
<filterStatement>
<query>WHERE parentId IS NULL LIMIT 500</query>
</filterStatement>
</getAdUnitsByStatement>
</soapenv:Body>
</soapenv:Envelope>
Это огромная нагрузка для того что б вернуть этот аргумент:
<query>WHERE parentId IS NULL LIMIT 500</query>
В JavaScript, что будет выглядеть следующим образом:
/* Signature */
function getAdUnitsByStatement(filterStatement) {
// ...
};
/* Usage */
getAdUnitsByStatement('WHERE parentId IS NULL LIMIT 500');
В формате JSON API, это может выглядеть следующим образом:
POST /getAdUnitsByStatement HTTP/1.1
HOST: api.example.com
Content-Type: application/json
{"filter": "WHERE parentId IS NULL LIMIT 500"}
Даже если этот код гараздо меньше, мы по-прежнему должны иметь в виду различные методы для getAdUnitsByStatement и getAdUnitsBySomethingElse. REST выглядет лучше, когда вы рассматриваете эти примеры, так как это позволяет генерировать общие конечные точки в строке запроса (например, GET /ads?statement={foo} или GET /ads?something={bar}). Вы можете комбинировать элементы в строке запроса, чтобы получить /ads?statement={foo}&limit=500, а в ближайшее время избавимся от этого странного синтаксиса SQL.
В этой статье мы не будем пытаться выделить что «лучшее», а, скорее, поможем вам принять обоснованное решение о том, когда один из подходов может быть более хорош.
Для чего они?
RPC на основе API-интерфейсы прекрасно подходят для действий (процедуры или команды).
REST на основе API, прекрасно подходят для моделирования вашего домена, что делает CRUD (создание, чтение, обновление, удаление) доступными для всех ваших данных.
REST не только для CRUD, но все операции делаются главным образом на основе CRUD. REST будет использовать методы HTTP, такие как GET, POST, PUT, DELETE, OPTIONS и PATCH.
RPC, однако, использует только GET и POST, где GET используется для получения информации, а POST для всего остального. Вы часто могли наблюдать когда для RPC API используется это: POST /deleteFoo, с телом { «id»: 1 }, вместо REST, для которого бы прописывалось DELETE /foos/1.
Это не является важным отличием, это просто деталь реализации. Самая большая разница на мой взгляд, в том, как действия обрабатываются. В RPC вы просто имеете POST /doWhateverThingNow, и это довольно ясно. Но с REST со своими CRUD-подобными операциями вы можете почувствовать, что REST не подходит не для чего кроме CRUD обработки.
На самом деле это не совсем так. Инициирующие действия можно сделать с помощью любого подхода. Например, если вы хотите «Отправить сообщение» для пользователя, RPC будет следующим образом:
POST /SendUserMessage HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"userId": 501, "message": "Hello!"}
Но в REST, то же самое действие будет таким:
POST /users/501/messages HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"message": "Hello!"}
Представьте себе приложение Carpooling, который имеет «trips». Эти trips должны иметь действия «start», «finish» и «cancel», или иначе пользователь никогда не будет знать, когда они начали или закончили.
В REST API, у вас уже есть GET /trips и POST /trips:
- POST /trips/123/start
- POST /trips/123/finish
- POST /trips/123/cancel
Этот кроссовер является признаком того, как сложно вносить действия в REST. Один из подходов заключается в использовании чего-то вроде поля состояния:
PATCH /trips/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"status": "in_progress"}
Как и в любой другой области, вы можете получать новое значение статуса в запросе PATCH и, исходя из этого, выполнять нужные действия:
module States
class Trip
include Statesman::Machine
state :locating, initial: true
state :in_progress
state :complete
transition from: :locating, to: [:in_progress]
transition from: :in_progress, to: [:complete]
after_transition(from: :locating, to: :in_progress) do |trip|
start_trip(trip)
end
after_transition(from: :in_progress, to: :complete) do |trip|
end_trip(trip)
end
end
end
В принципе, здесь, в ваших контроллерах, в коде lib или DDD-логике, вы можете проверить, было ли передано «статус» в запросе PATCH, и если да, вы можете попробовать перейти на него:
resource.transition_to!(:in_progress)
Когда этот код будет выполнен, он либо успешно выполнит переход, либо выполнит любую логику в блоке after_transition, либо выбросит ошибку.
Действиями success могут быть любые: отправка электронной почты, отключение push-уведомления, обращение к другой службе, чтобы начать просмотр местоположения GPS-устройства водителя, чтобы сообщить, где находится автомобиль — все, что вам нравится.
Не было необходимости в методе POST/startTrip RPC или в REST-овом POST/trip/123/start , потому что его можно было просто последовательно обрабатывать в рамках соглашений REST API.
Альтернатива
Мы видели здесь два подхода корректирующих действия внутри REST API, не нарушая его REST-правил. В зависимости от типа приложения, для которого строится API, эти подходы могут быть менее логичными и более похожими на велосипеды. Можно начинать задаваться вопросом: почему я пытаюсь замять все эти действия в REST API? API RPC может быть отличной альтернативой, или это может быть новая услуга, дополняющая существующий REST API. Slack использует веб-API на основе RPC, потому что то, над чем он работает, просто не вписывается в REST. Представьте, что вы пытаетесь предоставить опции «kick», «ban» или «leave» для того, чтоб пользователи могли покинуть или удалиться из одного канала или из всей команды Slack, используя только REST:
DELETE /users/jerkface HTTP/1.1
Host: api.example.com
DELETE кажется наиболее подходящим HTTP-методом для использования, но этот запрос настолько расплывчатый. Это может означать закрытие учетной записи пользователя полностью, что может сильно отличаться от запроса пользователя. Это явно не «kick» или «leave». Другим подходом может быть попытка PATCH:
PATCH /users/jerkface HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"status": "kicked"}
Это было бы странно, потому что статус пользователя не был бы глобально изменен для всего, поэтому ему понадобилась бы дополнительная информация, переданная ему, чтобы указать канал:
PATCH /users/jerkface HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"status": "kicked", "kick_channel": "catgifs"}
В таком случае создается новое произвольное поле, которое передается, и это поле фактически не существует для пользователя. Отказавшись от такого подхода, мы могли бы попробовать работать с отношениями:
DELETE /channels/catgifs/users/jerkface HTTP/1.1
Host: api.example.com
Это немного лучше, потому что мы больше не возимся с ресурсом global/users/jerkface, но он по-прежнему не имеет опции «kick», «ban» или «leave» и помещает это в тело или строку запроса.
Единственный другой подход, который приходит на ум, — создать коллекцию «kicks», коллекцию «leaves» и коллекцию «bans», с некоторыми запросами для POST/kicks, POST/bans и POST/leaves.
Slack Web API выглядит так:
POST /api/channels.kick HTTP/1.1
Host: slack.com
Content-Type: application/json
{
"token": "xxxx-xxxxxxxxx-xxxx",
"channel": "C1234567890",
"user": "U1234567890"
}
Легко и просто! Мы просто отправляем аргументы для этой задачи, как и на любом языке программирования, который имеет функции.
Одно простое правило:
- Если API — это в основном действия, возможно, это должен быть RPC.
- Если API в основном CRUD и манипулирует связанными данными, возможно, он должен быть REST.
Что, если ни один из них не станет явным победителем? Какой подход вы выбираете?
Использование обоих — REST и RPC
Идея, что вам нужно выбрать один подход и иметь только один API, — это немного ложь. Приложение может очень легко иметь несколько API или дополнительных сервисов, которые не считаются «основным» API. С помощью любого API или сервиса, который предоставляет HTTP-запросы, у вас есть выбор между правилами REST или RPC, и, возможно, у вас будет один REST API и несколько RPC-сервисов. Например, на конференции кто-то задал этот вопрос:
У нас есть REST API для управления веб-хостинговой компанией. Мы можем создавать новые экземпляры серверов и назначать их пользователям, что хорошо работает, но как мы перезапускаем серверы и запускаем команды по партиям серверов через API по RESTful?
Нет никакого реального способа сделать это, что не является ужасным, кроме создания простой службы типа RPC, которая имеет метод POST / restartServer и метод POST / execServer, который может быть выполнен на серверах, построенных и поддерживаемых через сервер REST.
Итого
Знание различий между REST и RPC может быть невероятно полезным, когда вы планируете новый API, и это может реально помочь, когда вы работаете над функциями для существующих API. Лучше всего не смешивать стили в одном API, потому что это может смущать как потребителей вашего API, так и любых инструментов, которые ожидают один набор соглашений (например, REST), и которые падают, когда вместо этого он видит другой набор соглашений (RPC). Используйте REST, если это имеет смысл, или используйте RPC, если это более уместно. Или используйте оба и получите лучшее из обоих миров!
Источник: Smashing magazine