Комментарии 57
Спасибо за статью! Очень актуально для меня именно в данный момент времени. Жду вашего перевода второй части :)
>> Это в свою очередь значит, что ваши запросы могут кэшироваться любым промежуточным прокси (снижение нагрузки).

Тут надо отметить, что кеш бывает public и private. Поэтому вполне допустимо иметь метод /me/products который для каждого пользователя возвращает разные данные.
Может и так, но я не автор, я просто перевел в силу возможностей и знаний.
Вот кстати у меня вопрос про /me. Как он соотносится со stateless? Ведь равно создается сессия и сервер обрабатывает запрос в контексте сессии пользователя.
/me/products не соответствует принципу stateless т.к. Подразумевает, что сервер знает что это за me.

Правильно будет /users/gnomeby/products
Stateless-принцип просто означает, что сервер не должен хранить и использовать состояние клиента.

«Each request from client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server.» — первоисточник

Поэтому если пользователь однозначно идентифицируется через один из заголовков запроса (Autorization, например), то вполне можно использовать /me. Это часть взаимодействия, а не состояние клиента.

В ответ, можно например, сделать редирект на /users/gnomeby/products либо даже напрямую выдавать ответ. В последнем случае нужно не забывать дописать используемый заголовок-идентификатор в Vary ответа.
Пропустил, это я себе пометку сделал в процессе. Исправил, спасибо.
На хабре с 2010 года, и до сих пор непонятно где ссылки на оригинал? Вы заебали, товарищи.
Это скорее повод сделать ссылку более заметной для читателей
спасибо большое за статью, сам тоже начинающий, жду вторую часть…
В части передачи пароля какая-то странная каша.
То что вы передаете хэш пароля от клиента который сравнивается с хэшом пароля на сервере, по большому счету тоже самое что передавать пароль текстом и сравнивать его с паролем.
До какой-то степени это может обезопасить пользователей, у которые используют один пароль на всех сайтах, но конкретно вашему сервису это не дает ровным счетом ничего.
Дает, если, например, через API нельзя менять пароль.

Злоумышленник получив хэш сможет лишь использовать API, а хозяин, заметив неладное, может зайти через сайт и сменить пароль.
Вообще хранить сами пароли на сервере это зло, я за хеш, при том что к паролю еще добавляется соль.
Я лично как пользователь перестану пользоваться ресурсом, если узнаю что там хранится мой пароль без изменений.
Да и краснеть не придется, если вашу БД взломают и стянут все логины с паролями.
Ну и самое главное нужно думать о пользователях, а большинство такое, что логины и пароли у них повторяются.
То, что касается разделения приложений на версии, я подробно расписывал в habrahabr.ru/post/133989/ («Система настроек и смена версий программ»). Тогда, видимо, было воспринято как очень сложное, здесь будет ближе к теме.
Понимаю, что перевод, поэтому вопрос не автору, а переводчику и знающим людям :-)
В статье сказано:
>DELETE абсолютно однозначен. Он индемпотентен как и PUT, и должен использоваться для удаления записи если таковая существует.
Объясните, пожалуйста, почему DELETE считается индемпотентным. Ведь после второго вызова объект не удалится (если при первом вызове удалился) и это будет уже не равнозначные вызовы.
После двух подряд одинаковых вызовов DELETE состояние хранилища не изменится, как и при двух подряд одинаковых вызовах PUT, в отличии от вызовов POST.
Мы часто путаемся из-за того, что есть состояние клиента и состояние сервера.

Собственно, «idempotence» в мире REST чаще всего используется по отношению к состоянию сервера и означает, что при нескольких применениях одного и того же метода сервер остается в одном и том же состоянии. Хотя по факту на клиенте вы можете получить разные ответы.

А тот же stateless-принцип просто означает, что нельзя держать состояние клиента на сервере (а точнее — менять поведение сервера в зависимости от состояния клиента).
Про версионность моделей красиво.
А что делать с версионностью базы данных?
Как поддерживать старую версию, если структуру надо менять?
Маппинг менять, если изменения вроде переноса столбца в другую таблицу. Хуже когда, например, нужно преобразование связи один-к-одному в один-ко-многим, тут уже волевое решение надо применять о том, что будет отдаваться в старой версии.
Можно использовать документо-ориентированные БД, они ж scheme-free. Это поможет, если структуру нужно только дополнять.
Красивая версионность — это в HTML. Будущее версионности API видимо тоже за подобным подходом — когда модель будет дополнительно описываться некоторыми тэгами или другими метаданными. При этом незнакомые тэги будут игнорироваться клиентом, а в случае несовместимых изменений будут вводиться новые тэги. Тогда старые клиенты смогут продолжать работу хотя бы частично.
Интересно, а какое отношение к технологии Rest имеет этот хипстерский пафос,- «В 2007-м Стив Джобс представил iPhone, который произвел революцию в высокотехнологичной индустрии и изменил наш подход к работе и ведению бизнеса. »?
Тем, что RESTful API нужен для приложений, и все хипстеры-стартаперы обязаны иметь rest api для свего приложение и ios приложение которое использует это api, иначе в инстаграме забанят.
«Что как бы характеризует отношение автора к PHP.» Ваше отношение к PHP мало кого интересует. Мне нравится PHP, Java, но НЕ нравится Ruby, но я никогда не отзывался о последнем отрицательно, потому как это сугубо личное дело каждого.
Достаточно было просто опустить этот момент.

В целом статья интересна. Спасибо.
Несмотря на «Ваше отношение к PHP мало кого интересует» я все таки скажу, что сам пишу на PHP и вполне им доволен, ничего против не имею и холиварить не собирался. Это небольшая попытка юмора автора, которую я в свою очередь попытался перевести.
Обновил статью, добавив ссылку на тот пост, там досталось всем языкам, не только PHP. Так что не сочтите мое отступление оскорблением
С аутентификацией конечно в REST сложно. Столько статей прочёл, но так и не понял как правильно её делать.
Тоже этот вопрос волнует. Тем более, что у меня в разработке сервис, в котором данные наполняются машинами, а читаются людьми. И тех и тех нужно авторизовывать.
А в чем проблема? Объясните свои затруднения. Авторизация не есть «состояние клиента» — это обязательная часть взаимодействия.

Поэтому OAuth, HTTP Basic, HTTP Digest — всё это правильные способы аутентификации для REST. Даже подписанная Cookie, содержащая ID пользователя подойдет.

С Cookie другая проблема — она часто используется именно для передачи ID сессии, хранящей клиентское состояние. Но если вы не используете сессию, то Cookie можно использовать как «workaround» против невозможности скормить браузеру собственный заголовок Authorization.

Если у вашего API это задокументировано, то всё нормально. Из той же области, что и использование параметра _method с POST запросом, когда PUT и DELETE не поддерживаются браузером.
Я скорее не про выделенное API для клиентов, а про обычный сайт, использующий принципы REST, где API получается как следствие простым использованием GET /posts/%id%.json вместо GET /posts/%id%.html. И да, неразрывно связывая аутентификацию и сессию.
Я не против REST подхода в целом, но нужно понимать, что он имеет и свои недостатки, в том числе и для примера в статье:
Что может пойти не так в приведенном случае, так это то, что если ваш пользователь использует сервис с двух или трех устройств, то, в случае когда одно из них устанавливает последний прочитанный элемент, то остальные не смогут загрузить элементы ленты, прочитанные на других устройствах ранее.

Независимость от состояния означает, что данные, возвращаемые определенным вызовом API, не должны зависеть от вызовов, сделанных ранее.

Зато при REST пользователь прочтя ленту на одном клиенте, при переходе на другой должен заново просматривать всю ленту в поисках последнего прочитанного элемента, что может занять чуть ли не больше времени, чем собственно просмотр новых элементов. Пример — твиттер.
В статье написано верно — клиент должен получать сообщения относительно временной метки, дабы загружать _все_ твиты. Иначе, при запросе, допустим /get-last, он будет получать только новые и в ленте будут пробелы.
Другое дело, что каждое сообщение должно помечаться меткой прочитано/не_прочитано, тогда загрузятся все сообщения, а непрочитанные будут выделены в интерфейсе клиента.
Другое дело, что каждое сообщение должно помечаться меткой прочитано/не_прочитано, тогда загрузятся все сообщения, а непрочитанные будут выделены в интерфейсе клиента.

В таком варианте опять-таки GET-запрос /get-last будет во-первых изменять состояние сервера, а во вторых ответ на этот запрос будет зависеть от предыдущих вызовов. И то и то противоречит REST.
Я наверное некорректно выразился: /get-last — неправильный запрос. Надо использовать описанный в статье /feed?lastFeed=20120228, то есть с указанием диапазона получаемых сообщений.
В этом случае состояние сервера не изменяется.
Не важно, какой запрос. Если состояние сервера от запроса не изменится, то сервер не запомнит, какие сообщения были прочитаны, и не сможет их пометить при следующем запросе (но с другого устройства).
Всё верно. Пока пользователь сообщение не прочитал — оно не прочитано. Когда прочитал — делаем PUT на сервер и помечаем как прочитанное. Что не так?
Это и есть недостаток. Дополнительный запрос на сервер там, где без него можно элементарно обойтись.
И даже в этом случае всё равно формально принцип statelessness соблюдаться не будет: на один и тот же запрос /feed?lastFeed=20120228 могут приходить разные ответы (в одном одни сообщения помечены как непрочитанные, в другом — другие).
Давайте лучше на примере, а то мы, возможно, говорим немного о разном.

Пусть будет приложение для чтения RSS ленты (типа google reader). Есть декстоп и мобильное приложение для планшета. Есть лента с пронумерованными постами (нумерация увеличивается по мере добавления сообщений).

В ленте 100 сообщений, 20 — непрочитанных.
На десктопе отображается: 100 сообщений (20 новых) — актуальная версия.
На планшете: 70 сообщений (0 новых) — до обновления данных.

Обновляю данные на планшете: GET /posts?last=70, получаю: {all:30, new:20}.
Если я выполню запрос GET /posts?last=70 ещё раз, ответ будет точно таким же, т.к. данные не изменились.
Читаю одно сообщение: PUT /post/81?read=1
Планшет обновляется: GET /posts?last=81, ответ: {all:19, new: 19}

Переключаюсь на десктоп, он это палит и запрашивает обновление: GET /posts/last=80, получает: {all:20, new: 19}

Из примера видно:
— состояние сервера не меняется, клиенты сами знают сколько у них сообщений сейчас у сами формируют запрос на получение актуальной информации.
— на один и тот же запрос будет приходить один и тот же ответ до тех пор, пока не изменяться данные в БД

Вопросы к вам:
— почему вы считаете, что ответы разные на один и тот же запрос?
— как можно обойтись без PUT (вы ведь его имели ввиду под словами «лишний запрос»)?
У вас в примере всё хорошо (почти). Но я твиттер как пример приводил, для которого просто глупо на каждый твит слать отдельный запрос.
Все твиты логичнее слать в одном запросе, при REST это:
планшет GET /posts?last=70, ответ: {{tweet71},{tweet72},{tweet100}}
Потом, пока я переключаюсь на десктоп, приходит ещё десяток-другой твитов, кроме того, последний раз на десктопе я с утра читал только 10й твит. Получаем
десктоп GET /posts?last=10 ответ:{{tweet11},...,{tweet120}}
Итого, вместо последних 20 твитов, я получаю 110, среди которых я вынужден искать последний прочитанный. И поиск зачастую длится дольше просмотра непрочитанных элементов. Пример, кстати, не синтетический, а отражает личный опыт чтения твиттера на iPad и десктоп.
Можно конечно и PUT-запрос слать:
планшет PUT /posts?lastread=100
Но зачем? Ради формального соответствия REST?

Кроме того, при таком API я всё равно получу 110 элементов, пусть они и будут разделены на прочитанные/новые.
А в идеале мне, как пользователю, нужен вариант:
планшет GET /posts?new, ответ: {{tweet70,read:1},{tweet71:read:0},{tweet72,read:0},{tweet100,read:0}}
десктоп GET /posts?new ответ:{{tweet100,read:1},...,{tweet120,read:0}}

Т.е. грузятся только новые плюс один-два-три старых твита, чтоб вспомнить, на чем остановился читать. Строгий REST этого обеспечить не может.
Вы предлагает помечать сообщение как прочитанные при их загрузке? То есть если телефон автоматически синхронизирует ленту, но я ее не стану смотреть, то на десктопе, я этих твитов уже не увижу?
Да, я предлагаю помечать сообщения как прочитанные в момент, когда они показываются пользователю в открытом виде, т.е. если я набираю twitter.com, подгружаются твиты, то они должны быть помечены как прочитанные.
То есть если телефон автоматически синхронизирует ленту, но я ее не стану смотреть, то на десктопе, я этих твитов уже не увижу?

Что значит «не увижу»? Пролистнули ленту вниз, и подгрузились более ранние сообщения. Никто же не предлагает ограничивать API одним /posts?new
Вы рассматриваете клиент как архиватор абсолютно всех сообщений? Я — как средство удобного слежения за непрочитанными. Для первого REST подходит полностью, для второго — нет.
Так чтобы подгрузились ранние сообщения, нужно будет делать еще один запрос и в чем тогда преимущество вашего решения? Кроме, того пользователю будут показываться не с реально последнего прочитанного сообщения, а с последнего загруженного и все равно прийдется листать, только теперь не вперед, а назад. Пользователями API могут быть не только люди, но и другие программы, а в таком случае может и не быть потребности отмечать загруженные сообщения, как прочитанные.
Преимущество в том, что ранние (вероятнее всего прочитанные) сообщения подгружать нужно редко. См. пример 20 твитов vs 110 твитов.
В целом предлагаю проектировать API ориентируясь в первую очередь на бизнес-требования (чтобы пользователям было удобно), а не на удобство программисту и следование формальным стандартам. Точнее так: следовать стандарту, но осознавать все его недостатки, и в случае необходимости делать исключения.
может и не быть потребности

/posts?last=70&markasread=0 например решит проблему
«вероятнее всего прочитанные» — у меня на телефоне автоматическая синхронизация включена, на десктопе тоже запущен клиент, который постоянно тянет новые твиты. Так, что в моем случае вероятнее всего новые сообщения будут разбросаны между устройствами.
API он для программистов, а не для пользователей.
Но с выводом, что не нужно слепо следовать стандарту, согласен.
НЛО прилетело и опубликовало эту надпись здесь
Я надеюсь про HATEOAS во второй части будет инфрмация?
Будет, но кратко в начале. Планирую до конца недели выложить вторую часть. Перевод готов, осталось оформить.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.