- PVSM.RU - https://www.pvsm.ru -
Всем привет! Меня зовут Дмитрий Павлов, в компании Align Technology мы с коллегами занимаемся разработкой Web API для взаимодействия внутренних систем и интеграции нашей компании со сторонними вендорами. Об идеях создания API для веба, а точнее RESTful API я хотел бы рассказать в этой статье.
В последние годы тема Web API стала очень популярна, многие компании занимаются созданием подобных интерфейсов, как открытых, так и для внутреннего пользования. В описании Web API практически всегда можно встретить акроним REST, но что же это обозначает этот термин и правильно ли его используют?
Большинство разработчиков, особенно в России, понимает под REST программный интерфейс, работающий с использованием протокола HTTP[S] при условии соблюдения следующих свойств:
Сервер не хранит состояние клиента: никаких сессий, все что требуется для выполнения запроса клиент передаёт с самим запросом.
Человекочитаемые URL, в которых ресурсы идентифицируются отдельно. Никаких больше /index.php?productId=1
, вместо этого используем /products/1
Более широкое использование HTTP методов: не ограничиваемся GET и POST, добавляем PUT и DELETE. В некоторых API можно встретить еще и PATCH.
Алгоритм по которому такой API используется обычно стандартен. Для начала нужно пойти на сайт с документацией, найти страничку со списком URL-шаблонов для доступа к ресурсам. Обычно она выглядит так:
Список ресурсов для API "Рецепты печенек"
--- /recipes/cookies - список рецептов печенек,
GET на данный URL возвращает список доступных рецептов
[
{
"name" : "Овсяное печенье с шоколадом",
"rating" : 5,
"shortDescription" : "...."
}
]
POST на данный URL возволит вам создать новый рецепт. В качестве тела запроса ожидается json вида как
{
"name" : "Малиновое печенье",
"shortDescription" : "...."
......
}
--- /recipes/cookies/:name - рецепт печеньки с именем ${name}
{
"name" : "Овсяное печенье с шоколадом",
"rating" : 5,
"shortDescription" : "....",
"description" : "...."
"ingredients" : [
{
"name" : "Овсянка",
.....
},
{
"name" : "Масло",
.....
},
{
"name" : "Шоколад",
.....
}
],
"cookingSteps" : [
....
]
}
// остальные ресурсы гипотетического API с перечсилением HTTP методов и URL шаблонов
и изучив её выполнять запросы к ресурсам (что обычно выражается в написании клиента, который по заданным форматам URL'ов подставляет параметры и обрабатывает ответы).
Примеров таких API на просторах сети предостаточно, до недавнего времени у Яндекса многие API (раз [1], два [2]) заявленные как REST работали по этой схеме.
Если мы обратимся к первоисточникам, т.е. к диссертации Роя Филдинга (на которую очень часто ссылаются, но гораздо реже читают), мы увидим, что API, созданные таким способом, не могут называться REST, поскольку они нарушают некоторые из принципов, описанных в диссертации, самый главный из которых — использование hypermedia как средства управления состоянием (Hypermedia As The Engine Of Application State ,HATEOAS), косвенно затрагивая вопросы само описываемых сообщений (self-descriptive messages).
Суть HATEOAS состоит в подходе к описанию ресурсов нашего API. Вместо простого перечисления набора ресурсов, со списком всех возможных операций, которые клиент может вызвать, руководствуясь некоторой внутренней логикой, мы проводим инверсию контроля — теперь за состояние ресурса отвечает сервер и он диктует клиенту, какие операции над ресурсом можно совершить в текущий момент. Эта информация должна присутствовать в самом представлении ресурса, который получает клиент. Таким образом, представление ресурса само себя описывает в достаточной степени, чтобы клиент понял, что с ним можно делать.
Применение такого подхода обычно означает, что клиент знает некоторый конечный набор "точек входа" (можете считать их аналогами стартовых страниц на сайтах), с которых он начинает свое взаимодействие с API, используя предоставленную в представлении ресурса информацию для навигации к другим ресурсам и совершения действий.
Для достижения этой задачи как раз и используются гиперссылки (hypermedia):
Отсутствие ссылки как на связанные ресурсы, так и на доступные действия означает, что данная операция недоступна в текущем состоянии ресурса.
Возвращаясь к примеру с нашим API о каталоге рецептов печенек, преобразуем его в Hypermedia-вид.
Как вы помните, у нас был список рецептов и ресурс, подробно описывающий конкретный рецепт с перечислением ингредиентов и шагов по приготовлению. Вот как они будут выглядеть с использованием hypermedia подхода:
//список рецептов
{
"links": {
"self" "/recipes/cookies"
}
"items": [
{
"name": "Овсяное печенье с шоколадом",
"rating": 5,
"shortDescription": "...."
"links": {
"self": "/recipes/cookies/Овсяное печенье с шоколадом"
}
}
]
}
//конкретный рецепт
{
"links": {
"self": "/recipes/cookies/Овсяное печенье с шоколадом"
}
"name": "Овсяное печенье с шоколадом",
"rating": 5,
"shortDescription": "....",
"description": "...."
"ingredients": [
{
"name": "Овсянка",
.....
},
{
"name": "Масло",
.....
},
{
"name": "Шоколад",
.....
}
],
"cookingSteps": [
....
]
}
Значительное отличие от оригинальной версии заключается в появлении объекта links
внутри каждого ресурса. Ключи этого объекта представляют собой relation'ы (те самые идентификаторы), а значения — ссылки. В результате наши ресурсы не требуют дополнительной информации (вне самого ресурса) о том как же перейти из каталога рецептов к детальному описанию, ссылка встроена в представление ресурса.
Данные подход позволяет легко расширять функциональность нашего API. Предположим, что мы для каждого рецепта хотим предоставить клиенту набор рекомендаций, представимый в виде списка рецептов. Сделать это очень легко, достаточно добавить в наш объект links новый ключ:
"links" : {
"self" : "/recipes/cookies/Овсяное печенье с шоколадом",
"http://acme.com/recipes/rels/you-can-also-like" : "/recipes/cookies?related_to=Овсяное+печенье+с+шоколадом"
}
Аналогично, совершенно не составит труда добавить идентификацию ингредиентов как отдельных ресурсов, если в этом возникнет необходимость.
Содержание URI не играет никакой роли, ведь теперь элементом API является relation, и мы без каких либо изменений на клиенте можем поменять ссылку на /recipes/related-to/Овсяное печенье с шоколадом
или на /recipes/234892skfj45sdlkfjdsa12
Hypermedia используется не только для навигации, но и для совершения действий, достаточно лишь определить, что некоторые relation отвечают за совершение определенных операций над ресурсами, а также обозначить семантику и детали этих операций.
Для наглядности рассмотрим пример с нашим API, добавив hypermedia-контрол для создания нового рецепта.
//список рецептов
{
"links" : {
"self" : "/recipes/cookies",
"http://acme.com/recipes/rels/add-recipe" : "/recipes/cookies"
}
"items" : [
.....
]
}
Мы лишь добавили ссылку со специальным relation'ом. Основное правило заключается в том, что клиент игнорирует неизвестные ему отношения: "старые" клиенты, не знающие как добавлять новый рецепт, будут работать как раньше, а для тех кто поддерживает создание, это будет сигналом, что есть возможность добавления нового рецепта путем отправки запроса на URI, который указан в отношении http://acme.com/recipes/rels/add-recipe
.
Данный подход позволяет нам не описывать статический набор операций и условия их выполнения в документации, а непосредственно серверу контролировать какие операции клиент может совершать над ресурсом в данный момент времени, а какие — нет. Добавление новых действий тоже не представляет сложности: мы просто объявляем новый relation, и начинаем включать его в представление ресурса, которое формирует сервер.
Разумеется, предоставление ссылок не снимает с сервера ответсвенности за корректное оперирование HTTP методами и соблюдения их семантики :).
У вас к данному моменту наверняка возник вопрос: какой смысл затевать все это, если для эффективной работы клиент все равно должен понимать смысл relation'ов? По ним-то документация должна иметься.
В самом деле, для эффективной работы клиент действительно должен понимать, что значит каждое отношение. Основная идея, стоящая за заменой интерпретации URI на работу с relation'ами, состоит в большей долговечности последних. URI является деталью реализации и может меняться со временем или от сервера к серверу. Relation же представляет собой семантическое описание связи и не завязан на детали хранения.
Предположим, я хочу сделать совместимый API для хранения рецептов, но из-за особенностей хранения хочу идентифицировать каждый рецепт по UUID, а не по названию. В случае с оригинальным API сделать это невозможно, а для hypermedia API это совершенно незаметно для клиента.
В результате появляется возможность создания более универсальных клиентов менее подверженных изменениям в случае модификаций на сервере.
Решив воспользоваться преимуществами Hypermedia подхода, мы модифицировали наш API указанным выше способом, и теперь у нас ресурсы связаны друг с другом по ссылке. С первого взгляда может показаться, что с нашим API все в порядке, но перед тем как заявить, что у нас Hypermedia API, посмотрим на заголовок Content-Type, возвращаемый нами в ответах. Если там стоит application/json
или даже text/plain
, то нам еще предстоит потрудиться.
Глядя на получившиеся у нас ресурсы, человек сразу выделяют ссылки, что создает впечатление о корректном формате нашего сообщения. Мы делаем вывод об этом, анализируя содержимое сообщения, тогда так стандарт предписывает смотреть на Content-Type заголовок ответа.
Рассмотрим следующий ответ сервера:
200 OK
Content-Type: text/plain
<?xml version="1.0"?>
<hello>world</hello>
Нам очевидно, что в ответе содержится xml-документ, но Content-Type предписывает воспринимать содержимое как простой текст, поэтому то, что он похож на xml-документ может быть просто совпадением или частным случаем. Именно поэтому верный Content-Type так важен.
Давайте разбираться, чем же для нашей задачи не подойдет application/json
? Дело в том, что стандарт, описывающий этот тип, не предусматривает никакого места или механизма для определения ссылок в нем. И даже если сформированное нами сообщения содержит ссылки, то машина не может отличить их от строки, в которой содержится текст по формату напоминающий ссылку. Нам же нужно однозначно определить, где в сообщении ссылка, а где нет, поэтому нам нужен другой тип.
Одним из способов решить проблему корректности Content-Type'а — использовать свой собственный. В документации мы явно укажем, где у нас в сообщении расположены ссылки. Если клиент получил от сервера ответ с нашем личным Content-Type'ом, ему не нужно будет динамически угадывать, что ссылка а что нет, если конечно он понимает наш Content-Type. Стоит отметить, что зачастую документация с описанием типа содержит не только подробности самого формата (т.е. где расположены ссылки, а где свойства), но и другую информацию:
Такие типы называются vendor specific, поскольку часто создаются под конкретную задачу и конкретной организацией. Их нет необходимости регистрировать в IANA. Рекомендуется давать им название вида application/vnd.${vendor}+${base_format}
, где ${vendor}
— это перевернутый домен компании, ${base_format}
— тип который мы взяли за основу. Если компания имеет домен acme.com и для представления наших ресурсов мы используем json, то для нашего API рецептов название типа будет выглядеть как application/vnd.com.acme.recipes+json
.
На первый взгляд, vendor specific типы решают возникшую проблему со ссылками, но у них есть и свои проблемы:
В качестве альтернативы не заставил себя ждать новый подход, который принесли типы общего назначения. Если подумать, то все что нам нужно от формата сообщений — это спецификация, отвечающая на вопросы:
Именно эта задача и решается: тип общего назначения не пытается подстроиться под конкретную доменную область, ими можно описать большинство ресурсов, с которыми мы имеем дело.
Важной особенностью всех типов общего назначения является то, что они не ставят задачу семантического описания документа, т.е. они не говорят, что же это за ресурс — описание рецепта или комментарий в блоге, это не их задача. Они отвечают больше за детали формата, оставляя семантическую спецификацию за рамками. Предполагается, что семантика будет заключена в так называемом профиле — отдельном документе, описывающим семантику свойств и отношений (relations).
На данный момент существует уже достаточно большое количество подобных форматов, поэтому перечислим лишь некоторые из них:
application/hal+json
— один из первых появившихся и наиболее популярный формат в наши дни;application/vnd.siren+json
;application/mason+json
.В описании всех таких форматов вы найдете, как и куда помещать свойства ресурса, в каком виде оформлять ссылки на другие ресурсы.
Различаются они форматом и возможностями, которые содержатся в самом типе.
Большинство типов общего назначения отличается незначительными деталями, например, как форматировать ссылки. Так, в HAL ссылки выглядят следующим образом:
"_links" : {
"self" : ....
"relToResource": .....
}
Тогда как Siren представляет их так:
"links" : [
{"rel" : ["self"], "href" : "...."},
{"rel" : ["relToResource"], "href" : "...."}
]
Основное отличие здесь в представлении relation значений. Создатель HAL стремился сделать формат более лаконичным, в то время как создатель Siren — более полным: relation у ссылки действительно может быть сложным (поэтому в Siren это массив значений), но это не всегда используется (поэтому в HAL это скаляр, да еще и ключ в объекте).
Такие вот разные взгляды и привели к созданию разных форматов, об одном формате договориться не смогли.
Не будем здесь перечислять все различия в форматах, обозначим только основные, на примере уже упомянутых типов:
application/vnd.error+json
), тогда как в Mason этот аспект включен в формат.Какой же вариант предпочтительнее: специально созданный тип или один из существующих вариантов? Как обычно бывает с подобными вопросами, однозначного ответа на него нет, все зависит от обстоятельств использования, поэтому попробуем выделить преимущества и недостатки каждого из них.
Одно из главных преимуществ hypermedia-типа общего назначения — экономия времени вам и клиентам вашего API. Вот за счет чего она достигается:
В то же время часть преимуществ такого подхода могут для кого-то выглядеть недостатками. В силу того, что тип не завязан под какую-либо доменную область и задачу, представление ресурсов получается более "раздутым", по сравнению со специальным типом, который мы могли бы создать.
В итоге для большинства задач можно рекомендовать использовать один из имеющихся Hypermedia-форматов общего назначения по умолчанию и делать выбор в пользу vendor-форматов в сложных или специфических случаях (если вы, конечно, не ставите целью vendor lock-in).
Описанный подход не является очередной серебряной пулей, призванной решить все проблемы при разработке API.
Можно отметить, что концепция точек входа способна привести к росту числа запросов, чтобы "добраться" до нужного ресурса и что включая ссылки мы делаем сообщение более объемным по сравнению с голыми данными.
На эти недостатки можно возразить, что эти проблемы решаются продуманной структурой ресурсов (кто мешает сделать операции поиска ресурса в точке входа для быстрой навигации?), кэшированием, которое тоже отмечено Филдингом как важная компонента этого архитектурного подхода, и банальным включением компрессии на веб серверах.
Основной плюс REST-подхода (здесь я имею ввиду полноценный REST) в гибкости и расширяемости, который он предоставляет, позволяя нам добавлять новые возможности или просто менять организацию ресурсов у себя на сервере без нарушения работы существующих клиентов.
Даже если вы решите не использовать hypermedia в вашем API, теперь вы знаете, что без нее REST — это не REST, а просто Web API. Это не делает API плохим или хорошим, я просто констатирую факт. Главное не забывать, что API мы делаем не ради самого API, а для решения задач, стоящих перед нами :).
Автор: Align Technology, R&D
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/api/117411
Ссылки в тексте:
[1] раз: https://tech.yandex.ru/pdd/doc/about-docpage/
[2] два: https://tech.yandex.ru/rasp/doc/concepts/about-docpage/
[3] API Яндекс Диска: https://tech.yandex.ru/disk/
[4] Github: https://developer.github.com/v3/
[5] Paypal: https://developer.paypal.com/docs/api/
[6] Foxycart: http://api.foxycart.com/docs
[7] представление документации: https://api.foxycart.com/hal-browser/index.html
[8] Источник: https://habrahabr.ru/post/281206/
Нажмите здесь для печати.