В данной статье описываются основы этой архитектуры, возможности и примеры её использования.
В общем случае REST является очень простым интерфейсом управления информацией без использования каких-то дополнительных внутренних прослоек. Каждая единица информации однозначно определяется глобальным идентификатором, таким как URL. Каждая URL в свою очередь имеет строго заданный формат.
А теперь тоже самое более наглядно:
Отсутствие дополнительных внутренних прослоек означает передачу данных в том же виде, что и сами данные. Т.е. мы не заворачиваем данные в XML, как это делает SOAP и XML-RPC, не используем AMF, как это делает Flash и т.д. Просто отдаем сами данные.
Каждая единица информации однозначно определяется URL – это значит, что URL по сути является первичным ключом для единицы данных. Т.е. например третья книга с книжной полки будет иметь вид /book/3, а 35 страница в этой книге - /book/3/page/35. Отсюда и получается строго заданный формат. Причем совершенно не имеет значения, в каком формате находятся данные по адресу /book/3/page/35 – это может быть и HTML, и отсканированная копия в виде jpeg-файла, и документ Microsoft Word.
Как происходит управление информацией сервиса – это целиком и полностью основывается на протоколе передачи данных. Наиболее распространенный протокол конечно же HTTP. Так вот, для HTTP действие над данными задается с помощью методов: GET (получить), PUT (добавить, заменить), POST (добавить, изменить, удалить), DELETE (удалить). Таким образом, действия CRUD (Create-Read-Updtae-Delete) могут выполняться как со всеми 4-мя методами, так и только с помощью GET и POST.
Вот как это будет выглядеть на примере:
GET /book/ - получить список всех книг
GET /book/3/ - получить книгу номер 3
PUT /book/ - добавить книгу (данные в теле запроса)
DELETE /book/3 – удалить книгу
ВАЖНОЕ ДОПОЛНЕНИЕ: Существуют так называемые REST-Patterns , которые различаются связыванием HTTP-методов с тем, что они делают. В частности, разные паттерны по-разному рассматривают POST и PUT. Однако, PUT предназначен для создания, реплейса или апдейта, для POST это не определено (The POST operation is very generic and no specific meaning can be attached to it) . Поэтому мой пример будет правильным и в таком виде, и в виде если поменять местами POST и PUT.
Вообще, POST может использоваться одновременно для всех действий изменения:
POST /book/ – добавить книгу (данные в теле запроса)
POST /book/3 – изменить книгу (данные в теле запроса)
POST /book/3 – удалить книгу (тело запроса пустое)
Это позволяет иногда обходить неприятные моменты, связанные с неприятием PUT и DELETE.
Для каждой единицы информации (info) определяется 5 действий. А именно:
GET /info/ (Index) – получает список всех объектов. Как правило, это упрощенный список, т.е. содержащий только поля идентификатора и названия объекта, без остальных данных.
GET /info/{id} (View) – получает полную информацию о объекте.
PUT /info/ или POST /info/ (Create) – создает новый объект. Данные передаются в теле запроса без применения кодирования, даже urlencode. В PHP тело запроса может быть получено таким способом:
Function getBody() {
if (!isset($HTTP_RAW_POST_DATA))
$HTTP_RAW_POST_DATA = file_get_contents("php://input");
return $HTTP_RAW_POST_DATA;
}
POST /info/{id} или PUT /info/{id} (Edit) – изменяет данные с идентификатором {id}, возможно заменяет их. Данные так же передаются в теле запроса, но в отличие от PUT здесь есть некоторый нюанс. Дело в том, что POST-запрос подразумевает наличие urldecoded-post-data. Т.е. если не применять кодирования – это нарушение стандарта. Тут кто как хочет – некоторые не обращают внимания на стандарт, некоторые используют какую-нибудь post-переменную.
DELETE /info/{id} (Delete) – удаляет данные с идентификатором {id}.
Еще раз отмечу, что в нашем примере /info/ - может и базироваться на какой-то другой информации, что может быть (и должно) быть отражено в URL:
/data/4/otherdata/6/info/3/ … и тому подобное.
Какие можно сделать из этого выводы:
Как видно, в архитектура REST очень проста в плане использования. По виду пришедшего запроса сразу можно определить, что он делает, не разбираясь в форматах (в отличие от SOAP, XML-RPC). Данные передаются без применения дополнительных слоев, поэтому REST считается менее ресурсоемким, поскольку не надо парсить запрос чтоб понять что он должен сделать и не надо переводить данные из одного формата в другой.
Архитектура REST позволяет серьезно упростить эту задачу. Конечно в реальности, того что описано не достаточно, ведь нельзя кому угодно давать возможность изменять информацию, то есть нужна еще авторизация и аутентификация. Но это достаточно просто разрешается при помощи различного типа сессий или просто HTTP Authentication.
Многие из вас уже наверняка знают о требованиях Джеффа Безоса к разработчикам в Amazon. Если вы не слышали об этом, ниже перечислены основные его положения:
В итоге эти требования оказались ключом к успеху Amazon. Компания смогла создавать эластичные системы, а позднее могла предложить эти услуги в лице Amazon Web Services.
Для разработки RESTful API нужно следовать следующим принципам:
Нужно убедиться в простоте базового URL для API. Например, если нужно разработать запрос для продуктов, должно получаться так:
/products /products/12345
Первый запрос к API - для всех продуктов, второй - для специфического продукта.
Многие разработчики совершают эту ошибку. Обычно они забывают, что у нас есть HTTP методы для лучшего описания API, и в итоге используют глаголы в URL. Например, запрос для получения всех продуктов звучит так:
/products
А не так:
/getAllProducts
Так часто поступают при создании URL.
В RESTful API существуют различные методы, которые описывают тип операции, которую будет осуществлять API.
Нужно обязательно убедиться в том, что вы используете верный HTTP метод для каждой операции.
Эта тема еще стоит под вопросом. Некоторым нравится называть URL ресурсов во множественном числе, некоторым - в единственном. Пример:
/products /product
Мне нравится использовать множественное число, потому что в таком случае не создается путаница: работаем ли мы с одним ресурсом или с группой ресурсов? Также не нужно дополнять базовые URL, например, добавлением all: /product/all .
Кому-то может не нравиться мой подход. Мой единственный совет - делайте так, чтобы в проекте все было единообразно.
Иногда нужен API, который должен работать не только по имени. В таком случае для разработки API понадобятся параметры запроса.
В таком случае URL не будут слишком длинными, а структура останется простой.
Существует множество HTTP кодов. Многие из нас используют только два из них: 200 и 500 ! Это плохая методика. Ниже перечислены часто используемые HTTP коды:
Версии API - важная вещь. Различные компании используют версии по-разному: кто-то - как даты, кто-то - как параметры запросов. Мне нравится указывать версии до названия ресурса. Пример:
/v1/products /v2/products
Мне также кажется, что стоит избегать использования /v1.2/products , так как это подразумевает, что API часто меняется. К тому же, точки в URL не так легко заметить. Так что чем проще, тем лучше.
Также хорошо бы сохранять совместимость с предыдущими версиями. В таком случае, если вы поменяете версию API, у пользователей будет время, прежде чем перейти на новую версию.
Обязательно нужно разбивать запрос на страницы, если вы работаете с API, который в качестве ответа может предоставить огромный объем данных. Если сбалансированность нагрузки не обеспечена, пользователь может обрушить сервер.
Всегда нужно иметь в виду, что структура API должна быть полностью защищена, в том числе от неосторожного обращения.
В таком случае стоит использовать limit и offset . Пример: /products?limit=25&offset=50 . Также стоит установить эти настройки по умолчанию.
Также нужно выбирать то, как будет отвечать API. Большинство современных приложений используют JSON. Приложения старых версий должны пользоваться XML ответами.
Всегда хорошо, чтобы у приложения был набор сообщений об ошибках, которые используются уместно. Например, в случае ошибки графические API на Facebook выдают такое сообщение:
{ "error": { "message": "(#803) Some of the aliases you requested do not exist: products", "type": "OAuthException", "code": 803, "fbtrace_id": "FOXX2AhLh80" } }
Я также видел некоторые примеры, в которых люди помещают в сообщения ссылку на ошибку, в которой рассказывается о ней и о способах избавления от проблемы.
Чтобы все группы разработчиков в компании подчинялись одним и тем же принципам, будет полезно использовать Open API Specification. Open API позволяет проектировать API и делиться ей с потребителями в более простой форме.
Довольно очевидно, что для лучшей связи отлично подойдут API. Если же они неудачно спроектированы, это вызовет только больше путаницы. Так что выкладывайтесь на полную в разработке, а дальше уже последует этап реализации.
Зачем, например, заморачиваться с методом DELETE или там заголовком Accept? Не проще ли использовать метод GET и передавать все в параметрах, например, delete=true или format=json ? Вбил в браузере, и работает! А вот этот ваш DELETE так просто через браузер не пошлешь. На что я ответил примерно так.
Вот, допустим, у вас есть некоторые ресурсы. Для определенности, пусть это будут книги и пользователи. Что, собственно, означает иметь REST API для работы с этими ресурсами? В первом приближении, следующее. Если мы хотим получить какую-то книгу, то говорим GET /books/123 . Аналогично информация о пользователе получается запросом GET /users/456 . Вообще-то, в начале URL неплохо бы иметь что-то вроде /api/v1.0/ , но для краткости мы это опустим. По умолчанию данные отдаются, например, в JSON’е , но при желании мы можем передать Accept-заголовок с другим форматом. Для создания или обновления существующей книги следует использовать метод PUT, передав данные в теле запроса и указав формат этих данных в заголовке Content-type. Для удаления данных используется метод DELETE.
Внимательный читатель спросит, а для чего тогда нужен POST? Вообще, если делать все по науке, он должен использоваться для добавления элементов в сущность, словно она является неким контейнером, например, словарем. Однако на практике так обычно не делают, ведь при использовании API несколькими клиентами один клиент может изменить название книги, а второй — ее цену, в результате чего получится ерунда. Поэтому POST либо вообще не используют, либо используют в качестве замены методов PUT и DELETE. То есть, POST с каким-то телом запроса работает, как PUT, а без тела запроса — как DELETE. Это позволяет работать с клиентами, которые почему-то не умеют посылать PUT и DELETE.
Можно работать и сразу с целыми коллекциями. Для получения списка всех пользователей говорим GET /users , а для создания нового пользователя с автоматически сгенерированным id — POST /users . Как и ранее, в последнем случае данные передаются в теле запроса. Также можно перезаписать всю коллекцию, сказав PUT /users , и удалить сразу всех пользователей, сказав DELETE /users . Еще иногда требуется фильтрация по полям или пагинация, в этих случаях делают так:
GET /api/v1.0/users?fields=id,email,url&offset=100&limit=10&order_by=id
… или как-то так:
GET /api/v1.0/logs?from=2013-01-01+00:00:00&to=2013-12-31+23:59:59
Как бы, это все. Довольно однообразно и даже логично, не так ли? Так чем такой подход лучше описанного в начале поста?
В свое время я имел удовольствие работать над проектом, где API был устроен «простым и понятным» образом, на методах GET и POST, со всякими delete=1 и так далее. Смею вас заверить, что на самом деле вы этого не хотите. Потому что на практике работа с этим API превращается в какой-то кошмар.
Допустим, один программист занимается книгами, а второй пользователями. Первый решает, что для получения списка всех сущностей будет использоваться запрос GET /all_books , а второй решает перечислять только id и использовать URL GET /select_user_ids . Для удаления сущности первый программист решает использовать параметр del=true , а второй — delete=1 . Для экспорта данных в CSV первый программист делает поддержку export=text/csv , а второй — format=CSV . Потом выясняется, что некоторые библиотеки не умеют посылать GET-запросы со слишком длинными query string и ходить за данными на чтение начинают методом POST. А затем кто-то случайно удаляет через браузер всех пользователей в боевом окружении… И так далее, и тому подобное, полный бардак в общем.
Вы спросите, что же мешает привести все это безобразие в одному стандарту, например, использовать только del=1 и export=csv ? Так вот, REST — это и есть то самое приведение к одному стандарту , с учетом всяческих граблей типа случайного удаления данных через браузер и так далее. Притом у разных компаний этот стандарт одинаковый. Когда в команду разработчиков приходит новичок, вы просто говорите ему, что у вас всюду REST, а основные ресурсы — это пользователи и книги. Все, после этого одного предложения ваш новый коллега знает 90% API, безо всякого там чтения Wiki. Если вы хотите говорить с иностранцами, вы же просто используете общепринятый английский язык , а не изобретаете новый? Вот так же и здесь. Нельзя также не напомнить о пользе повторного использования протоколов и кода. А ведь для работы с REST, и HTTP вообще, написана куча библиотек и фреймворков.
Вы скажите «я, конечно, согласен, что REST такой весь из себя интуитивно понятный и общепринятый, но что, если я просто хочу загрузить через браузер список книг в формате CSV»? Тут важно понимать, что REST — это не о том, как сделать все через браузер . Предполагается, что должен быть клиент, который умеет работать с вашим API, вот через него и экспортируете. Но если по каким-то причинам это затруднительно, вы можете, например, использовать curl. Если у вас нелады с консолью, вы без труда найдете множество GUI-клиентов или, скажем, какой-нибудь плагин для Chrome, с аналогичным функционалом. Однако я все же советую попробовать curl. Пользоваться им совсем не так сложно, как вам может казаться. Всего-то нужно запомнить десяток параметров.
Так задаются дополнительные HTTP-заголовки:
H "Accept: text/csv" -H "Content-type: application/json"
Выбираем используемый метод:
X{GET|PUT|POST|DELETE}
Указываем тело запроса:
D "{"name":"Alex","url":"http://сайт/"}"
D @filename.json
# чтобы при этом не удалялись символы новой строки:
--data-binary @filename.json
Выводим заголовки из ответа сервера в stdout:
Говорим передавать данные в gzip’е:
Сохраняем тело ответа в указанный файл вместо stdout:
Наконец, для отключения буферизации используйте флаг -N . Может пригодится, если вы работаете с большими объемами данных или бесконечными потоками.
Теперь рассмотрим пару примеров.
Экспорт книг в формате CSV:
curl -H "Accept: text/csv" http://localhost/api/v1.0/books -o books.csv
Создание пользователя c выводом заголовков из ответа сервера в stdout:
curl -XPOST -H "Content-type: application/json" -d "{"name":"Alex"}" \
http://localhost/api/v1.0/users -D -
Удаление пользователя с заданным id:
curl -XDELETE http://localhost/api/v1.0/users/123
Несложно, правда ведь?
Несколько финальных замечаний, относящихся не совсем к REST. Во-первых, иногда от приложения требуется не только предоставлять доступ к некоторым ресурсам, но и выполнять какие-то команды. Таким командам имеет смысл выделять URL-адреса, начинающиеся с /commands/ . Например, запуск почтовой рассылки по всем пользователям будет выглядеть как-то так:
curl -XPOST -H "Content-type: application/json" \
-d "{"subject":"Good news, everyone!","body":"..."}" \
http://localhost/api/v1.0/commands/notify_all_users_via_email
Дополнение: Некоторые команды должны быть доступны только в тестовом окружении, для них можно выделить URL-адреса, начинающиеся с /debug/ .
Во-вторых, иногда требуется реализовать бесконечные потоки событий , или отправку текущего состояния, а затем обновлений к нему. Таким концам разумно выделить URL, начинающиеся, например, со /streams/ . Вот как примерно это должно работать:
curl -H "Accept: application/x-json-stream" \
http://localhost/api/v1.0/streams/users -N
{"type":"user","data":{"id":123,"name":"Alex","url":"http://сайт/"}}
{"type":"user","data":{"id":456,"name":"Bob","url":"http://ya.ru/"}}
...
{"type":"sync"}
{"type":"heartbeat"}
{"type":"heartbeat"}
{"type":"user_deleted","data":{"id":123}}
...
Нужно обратить внимание на несколько моментов. Здесь используется формат x-json-stream , то есть, поток JSON-объектов, разделенных символом \n. Если этот символ встречается в самом JSON-объекте, его, соответственно, следует кодировать. Некоторым клиентам может быть удобнее работать с честным JSON’ом, то есть, списком JSON-объектов. Предусмотреть поддержку сразу нескольких форматов довольно просто. Во втором случае список объектов должен начинаться с открывающейся квадратной скобки, а объекты должны разделяться запятыми. Для удобства работы со стримом нужно либо ставить после запятых символ \n, либо делать это на стороне клиента с помощью sed:
curl ... | sed "s/},/}\n/g"
Каждый объект имеет поле type и опциональное поле data. Объекты с типом heartbeat посылаются несмотря ни на что один раз в пять секунд. Если клиент не видит такого объекта в течение десяти секунд, он считает, что либо что-то сломалось на стороне сервера, либо что-то не так с сетью, и закрывает соединение. Объект с типом sync используется в стримах, посылающих некое состояние, а затем обновления к нему, для разделения первого от второго. Наконец, все остальные типы представляют собой полезную нагрузку. Поле data нужно по той причине, что вложенные данные также могут иметь поле type, что приводило бы к неразберихе.
В-третьих, когда вы пишите RESTful приложение, старайтесь с самого начала придерживаться некоторых соглашений. Например, с самого начала договоритесь, что имена полей в JSON-объектах должны всегда писаться в camelCase. Раз и навсегда запретите использовать в идентификаторах такие спецсимволы, как знак плюс и пробелы. Договоритесь, что в случае получения кода 301 клиент должен посылать точно такой же запрос на URL, указанный в заголовке Location. Примите соглашение о том, как будет передаваться автоматически сгенерированные id. Например, в Riak для этого используется заголовок Location . Подумайте о том, как вы будете сообщать о различных типах ошибок, в том числе временной недоступности БД, ошибках валидации полей и так далее. Пользователи почти наверняка предпочтут увидеть:
{"message":"validation_error","description":"..."}
… вместо кода 500 без каких-либо дополнительных пояснений. Если для вашего приложения важна точность представления чисел, договоритесь передавать все числа в виде строк, чтобы json-декодер не терял точность из-за преобразования строк во float’ы.
Но помните, хотя все написанное выше — это идеал, к которому стоит стремиться, на практике всем наплевать на стандарты . А значит, вас ждет много подпорок, слепленных на скорую руку, нежелание коллег переходить на более правильные версии API (зачем, если все работает?), и многие другие увлекательные вещи.
RESTful API может создаваться не только для сторонних сервисов. Он может использоваться одностраничными приложениями для работы с бэк-эндом. Вот несколько основных моментов, которые нужно знать при проектировании интерфейса.
Ключевым принципом REST является деление вашего API на логические ресурсы. Управление этими ресурсами происходит с помощью HTTP-запросов с соответствующим методом - GET, POST, PUT, PATCH, DELETE.
Ресурс должен описываться существительным во множественном числе. Действия над ресурсами, обычно, определяются стратегией CRUD и соответствуют HTTP-методам следующим образом:
Если ресурс существует только в контексте другого ресурса, то URL может быть составным:
Когда действие над объектом не соответствует CRUD операции, то его можно рассматривать как составной ресурс:
Методы POST, PUT или PATCH могут изменять поля ресурса, которые не были включены в запрос (например, ID, дата создания или дата обновления). Чтобы не вынуждать пользователя API выполнять ещё один запрос на получение обновлённых данных, такие методы должны вернуть их в ответе.
Любые параметры в HTTP-запросе могут быть использованы для уточнения запроса или сортировки данных.
Когда нужно в ответ на запрос списка объектов добавить информацию о постраничной навигации, стоит воспользоваться HTTP-заголовком Link , а не добавлять обёртки данным.
Пример заголовка:
Link:
Возможные значения rel:
Важно следовать этим значениям, а не конструировать собственные URL-ы потому, что иногда постраничная навигация может основываться на сложных правилах, а не простом переборе страниц.
Для совместимости с некоторыми серверами или клиентами, которые не поддерживают другие HTTP-методы кроме GET и POST, может быть полезным их эмуляция. Значение метода передаётся в заголовке X-HTTP-Method-Override , а сам он выполняется как POST-метод. GET-запросы не должны менять состояние сервера!
В случае ошибок, в ответе может содержаться отладочная информация для разработчиков, если это возможно.