Приближение второе: Правильный путь
Если вдруг вы пропустили первое приближение, его можно найти здесь. А в этом приближении я хотел бы отдельно поговорить о подходах к построению уникальных путей к ресурсам и методам вашего web API и о тех архитектурных особенностях приложения, которые влияют на внешний вид этого пути и его компоненты.
О чем стоит подумать, стоя на берегу
Версионность
Рано или поздно любая действующая система начинает эволюционировать: развиваться, усложняться, масштабироваться, усовремениваться. Для разработчиков REST API это чревато в первую очередь тем, что необходимо запускать новые версии API при работающих старых. Здесь я говорю больше не об архитектурных изменениях под капотом вашей системы, а о том, что изменяется сам формат данных и набор операций с ними. В любом случае версионность нужно предусмотреть как в изначальной организации исходного кода, так и в принципе построения URL. Что касается URL, здесь существует два наиболее популярных способа указания версии API, которой адресован запрос. Префиксация пути example-api.com/v1/ и разведение версий на уровне субдомена v1.example-api.com. Использовать можно любой из них, в зависимости от потребности и необходимости.
Автономность компонентов
Web API сложных систем, поддерживающих несколько пользовательских ролей, зачастую требует разделения на части, каждая из которых обслуживает свой спектр задач. По сути, каждая часть может быть самостоятельным приложением, работать на разных физических машинах и платформах. В контексте описания API нам совершенно не важно, как сервер обрабатывает запрос и какие силы и технологии в этом замешаны. Для клиента API — система инкапсулированная. Тем не менее разные части системы могут обладать совершенно разной функциональностью, например, административная и пользовательская часть. И методология работы с одними и теми же, казалось бы, ресурсами может существенно отличаться. Поэтому такие части необходимо разделять на уровне домена admin.v1.example-api.com или префикса пути example-api.com/v1/admin. Это требование не является обязательным, и многое зависит от сложности системы и ее назначения.
Формат обмена данными
Самым удобным и функциональным, на мой взгляд, форматом обмена данными является JSON, но никто не запрещает использовать XML, YAML или любой другой формат, позволяющий хранить сериализованные объекты без потери типа данных (мы за типизацию). При желании можно сделать в API поддержку нескольких форматов ввода/вывода. Достаточно задействовать HTTP заголовок запроса для указания желаемого формата ответа Accept и Content-Type для указания формата переданных в запросе данных. Другим популярным способом является добавление расширения к URL ресурса, например, GET /users.xml, но такой способ кажется менее гибким и красивым, хотя бы потому, что утяжеляет URL и верен скорее для GET-запросов, нежели для всех возможных операций.
Локализация и многоязычность
На практике многоязычность API чаще всего сводится к переводу сервисных сообщений и сообщений об ошибках на требуемый язык для прямого отображения конечному пользователю. Многоязычный контент тоже имеет место быть, но сохранение и выдача контента на разных языках, на мой взгляд, должны разграничиваться более явно, например, если у вас одна и та же статья существует на разных языках, то по факту это две разных сущности, сгруппированные по признаку единства содержания. Для идентификации ожидаемого языка можно использовать разные способы. Самым простым можно считать стандартный HTTP заголовок Accept-Language. Я встречал и другие способы, такие, как добавление GET-параметра language=”en”, использование префикса пути example-api.com/en/ или даже на уровне доменного имени en.example-api.com. Мне кажется, что выбор способа указания локали зависит от конкретного приложения и задач, стоящих перед ним.
Внутренняя маршрутизация
Итак, мы добрались до корневого узла нашего API (или одного из его компонентов). Все дальнейшие маршруты будут проходить уже непосредственно внутри вашего серверного приложения, в соответствии с поддерживаемым им набором ресурсов.
Пути к коллекциям
Для указания пути к коллекции мы просто используем название соответствующей сущности, например, если это список пользователей, то путь будет таким /users. К коллекции как таковой применимы два метода: GET (получение лимитированного списка сущностей) и POST (создание нового элемента). В запросах на получение списков мы можем использовать множество дополнительных GET параметров, применяемых для постраничного вывода, сортировки, фильтрации, поиска etc, но они должны быть опциональными, т.е. эти параметры не должны передаваться как часть пути!
Элементы коллекции
Для обращения к конкретному элементу коллекции мы используем в маршруте его уникальный идентификатор /users/25. Это и есть уникальный путь к нему. Для работы с объектом применимы методы GET (получение объекта), PUT/PATCH (изменение) и DELETE (удаление).
Уникальные объекты
Во множестве сервисов существуют уникальные для текущего пользователя объекты, например, профиль текущего пользователя /profile, или персональные настройки /settings. Разумеется, с одной стороны, это элементы одной из коллекций, но они являются отправной точкой в использовании нашего Web API клиентским приложением, и к тому же позволяют намного более широкий спектр операций над данными. При этом коллекция, хранящая пользовательские настройки, может быть вообще недоступна из соображений безопасности и конфиденциальности данных.
Свойства объектов и коллекций
Для того, чтобы добраться до любого из свойств объекта напрямую, достаточно добавить к пути до объекта имя свойства, например получить имя пользователя /users/25/name. К свойству применимы методы GET (получение значения) и PUT/PATCH (изменение значения). Метод DELETE не применим, т.к. свойство является структурной частью объекта, как формализованной единицы данных.
В предыдущей части мы говорили о том, что у коллекций, как и у объектов, могут быть собственные свойства. На моей памяти мне пригодилось только свойство count, но ваше приложение может быть более сложным и специфичным. Пути к свойствам коллекций строятся по тому же принципу, что и к свойствам их элементов: /users/count. Для свойств коллекций применим только метод GET (получение свойства), т.к. коллекция — это только интерфейс для доступа к списку.
Коллекции связанных объектов
Одной из разновидностей свойств объектов могут быть связанные объекты или коллекции связанных объектов. Такие сущности, как правило, не являются собственным свойством объекта, а лишь отсылками к его связям с другими сущностями. Например, перечень ролей, которые были присвоены пользователю /users/25/roles. По поводу работы с вложенными объектами и коллекциями мы подробно поговорим в одной из следующих частей, а на данном этапе нам достаточно того, что мы имеем возможность обращаться к ним напрямую, как к любому другому свойству объекта.
Функции объектов и коллекций
Для построения пути к интерфейсу вызова функции у коллекции или объекта мы используем тот же самый подход, что и для обращения к свойству. Например, для объекта /users/25/sendPasswordReminder или коллекции /users/disableOld. Для вызовов функций мы в любом случае используем метод POST. Почему? Напомню, что в классическом REST не существует специального глагола для вызова функций, а потому нам придется использовать один из существующих. На мой взгляд, для этого больше всего подходит метод POST, т.к. он позволяет передавать на сервер необходимые аргументы, не является идемпотентным (возвращающим один и тот же результат при многократном обращении) и наиболее абстрактен по семантике.
Надеюсь, что все более-менее уложилось в систему:) В следующей части мы поговорим подробнее о запросах и ответах, их форматах, кодах статусов.
Комментарии (5)
savostin
26.10.2016 12:40Как правильнее:
GET /users/25
{ "id" : 25, "name" : "My Name", "roles" : [{"id" : 42, "name" : "Some"}] }
или
GET /users/25
{ "id" : 25, "name" : "My Name" }
GET /users/25/roles
[{"id" : 42, "name" : "Some"}]
Т.е. можно ли (и когда именно) включать связанные коллекции в ответ сервера? И какая вложенность?svovochka
26.10.2016 14:05На самом деле все три варианта имеют право на жизнь, плюс вариант с регулируемым набором полей в запросе, описанный TyVik ниже. Но везде есть ньюансы. Напомню, что мы говорим не о RESTfull API, а о Web-API, которое пересматривает концепт в пользу гибкости и универсальности, где возможности работы с данными лимитированы правами пользователя, как на получение набора данных, так и на запись, и методы работы с данными диктует серверная сторона.
1) Первый вариант возможен, если максимальное число ролей у пользователя лимитировано и не велико. Если их может быть миллион, то лучше запрашивать их отдельно, обращаясь к коллекции.
GET /roles?user_id=25. = GET /users/25/roles
2) Не во всех случаях клиент может иметь права на просмотр списка ролей, например, рядовой пользователь в большинстве случаев не может просматривать полную картину для другого пользователя.
sentyaev
Это потому, что в rest у нас не объекты и коллекции, а ресурсы. Соответственно у них нет функций.
Как вариант, можно сделать ресурс notification.
POST /notifications
{ userId: 25, type: 'password-reminder' }
Другой вариант.
POST /users/25/notifications
{ type: 'password-reminder }
Дополнительно вы можете работать с этим новым ресурсом.
Например получить все notifications которые pending:
GET /users/25/notifications/pending