На днях кое-кто спросил меня, дескать, зачем вообще нужен этот REST . Зачем, например, заморачиваться с методом 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
. Еще иногда требуется фильтрация по полям или пагинация, в этих случаях делают так:
… или как-то так:
Как бы, это все. Довольно однообразно и даже логично, не так ли? Так чем такой подход лучше описанного в начале поста?
В свое время я имел удовольствие работать над проектом, где 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-заголовки:
Выбираем используемый метод:
Указываем тело запроса:
Если тело запроса большое, можно сохранить его в файл и сказать:
# чтобы при этом не удалялись символы новой строки:
—data-binary @filename.json
Выводим заголовки из ответа сервера в stdout:
Говорим передавать данные в gzip’е:
Сохраняем тело ответа в указанный файл вместо stdout:
Наконец, для отключения буферизации используйте флаг -N
. Может пригодится, если вы работаете с большими объемами данных или бесконечными потоками.
Теперь рассмотрим пару примеров.
Экспорт книг в формате CSV:
Создание пользователя c выводом заголовков из ответа сервера в stdout:
http://localhost/api/v1.0/users -D —
Удаление пользователя с заданным id:
Несложно, правда ведь?
Несколько финальных замечаний, относящихся не совсем к REST. Во-первых, иногда от приложения требуется не только предоставлять доступ к некоторым ресурсам, но и выполнять какие-то команды. Таким командам имеет смысл выделять URL-адреса, начинающиеся с /commands/
. Например, запуск почтовой рассылки по всем пользователям будет выглядеть как-то так:
-d ‘{«subject»:»Good news, everyone!»,»body»:»…»}’
http://localhost/api/v1.0/commands/notify_all_users_via_email
Дополнение: Некоторые команды должны быть доступны только в тестовом окружении, для них можно выделить URL-адреса, начинающиеся с /debug/
.
Во-вторых, иногда требуется реализовать бесконечные потоки событий , или отправку текущего состояния, а затем обновлений к нему. Таким концам разумно выделить URL, начинающиеся, например, со /streams/
. Вот как примерно это должно работать:
http://localhost/api/v1.0/streams/users -N
{«type»:»user»,»data»:{«id»:123,»name»:»Alex»,»url»:»http://remontka.com/»}}
{«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:
Каждый объект имеет поле type и опциональное поле data. Объекты с типом heartbeat посылаются несмотря ни на что один раз в пять секунд. Если клиент не видит такого объекта в течение десяти секунд, он считает, что либо что-то сломалось на стороне сервера, либо что-то не так с сетью, и закрывает соединение. Объект с типом sync используется в стримах, посылающих некое состояние, а затем обновления к нему, для разделения первого от второго. Наконец, все остальные типы представляют собой полезную нагрузку. Поле data нужно по той причине, что вложенные данные также могут иметь поле type, что приводило бы к неразберихе.
В-третьих, когда вы пишите RESTful приложение, старайтесь с самого начала придерживаться некоторых соглашений. Например, с самого начала договоритесь, что имена полей в JSON-объектах должны всегда писаться в camelCase. Раз и навсегда запретите использовать в идентификаторах такие спецсимволы, как знак плюс и пробелы. Договоритесь, что в случае получения кода 301 клиент должен посылать точно такой же запрос на URL, указанный в заголовке Location. Примите соглашение о том, как будет передаваться автоматически сгенерированные id. Например, в Riak для этого используется заголовок Location . Подумайте о том, как вы будете сообщать о различных типах ошибок, в том числе временной недоступности БД, ошибках валидации полей и так далее. Пользователи почти наверняка предпочтут увидеть:
… вместо кода 500 без каких-либо дополнительных пояснений. Если для вашего приложения важна точность представления чисел, договоритесь передавать все числа в виде строк, чтобы json-декодер не терял точность из-за преобразования строк во float’ы.
Но помните, хотя все написанное выше — это идеал, к которому стоит стремиться, на практике всем наплевать на стандарты . А значит, вас ждет много подпорок, слепленных на скорую руку, нежелание коллег переходить на более правильные версии API (зачем, если все работает?), и многие другие увлекательные вещи.
Дополнение: Пишем REST-сервис на Python с использованием Flask