
Автор: Вячеслав Михайлов, Solutions Architect.
В этой статье я поделюсь опытом проектирования RESTful API — на конкретных примерах покажу, как делать хотя бы простые сервисы красиво. Также мы поговорим, что такое API и зачем он нужен, поговорим об основах REST — обсудим, на чем его можно реализовывать; коснемся основных веб-практик, которые зависят и не зависят от этой технологии. Также узнаем, как составлять хорошую документацию, затрачивая на это минимум усилий, и посмотрим, какие существуют способы нумерации версий для RESTful API.
Часть 1. Теория
Итак, как мы все знаем, API — application programming interface (интерфейс программирования приложений), набор правил и механизмов, с помощью которых одно приложение или компонент взаимодействует с другими
Почему хороший API — это важно?
- Простота использования и поддержки. Хороший API просто использовать и поддерживать.
- Хорошая конверсия в среде разработчиков. Если всем нравится ваш API, к вам приходят новые клиенты и пользователи.
- Выше популярность вашего сервиса. Чем больше пользователей API, тем выше популярность вашего сервиса.
- Лучше изоляция компонентов. Чем лучше структура API, тем лучше изоляция компонентов.
- Хорошее впечатление о продукте. API — это как бы UI разработчиков; это то, на что разработчики обращают внимание в первую очередь при встрече с продуктом. Если API кривой, вы как технический эксперт не будете рекомендовать компаниям использовать такой продукт, приобретая что-то стороннее.
Теперь посмотрим, какие бывают виды API.
Виды API по способу реализации:
- Web service APIs
- XML-RPC and JSON-RPC
- SOAP
- REST
- WebSockets APIs
- Library-based APIs
- Java Script
- Class-based APIs
- C# API
- Java
Виды API по категориям применения:
- OS function and routines
- Access to file system
- Access to user interface
- Object remoting APIs
- CORBA
- .Net remoting
- Hardware APIs
- Video acceleration (OpenCL…)
- Hard disk drives
- PCI bus
- …
Как мы видим, к Web API относятся XML-RPC и JSON-RPC, SOAP и REST.
RPC (remote procedure call — «удаленный вызов процедур») — понятие очень старое, объединяющие древние, средние и современные протоколы, которые позволяют вызвать метод в другом приложении. XML-RPC — протокол, появившийся в 1998 г. вскоре после появления XML. Изначально он поддерживался Microsoft, но вскоре Microsoft полностью переключилась на SOAP, поэтому в .Net Framework мы не найдем классов для поддержки этого протокола. Несмотря на это, XML-RPC продолжает жить до сих пор в различных языках (особенно в PHP) — видимо, заслужил любовь разработчиков простотой.
SOAP также появился в 1998 г. стараниями Microsoft. Он был анонсирован как революция в мире ПО. Нельзя сказать, что все пошло по плану Microsoft: было огромное количество критики из-за сложности и тяжеловесности протокола. В то же время, были и те, кто считал SOAP настоящим прорывом. Протокол продолжал развиваться и плодиться десятками новых и новых спецификаций, пока в 2003 г. W3C не утвердила в качестве рекомендации SOAP 1.2, который и сейчас — последний. Семейство у SOAP получилось внушительное: WS-Addressing, WS-Enumeration, WS-Eventing, WS-Transfer, WS-Trust, WS-Federation, Web Single Sign-On.
Затем, что закономерно, все же появился действительно простой подход — REST. Аббревиатура REST расшифровывается как representational state transfer — «передача состояния представления» или, лучше сказать, представление данных в удобном для клиента формате. Термин “REST” был введен Роем Филдингом в 2000 г. Основная идея REST в том, что каждое обращение к сервису переводит клиентское приложение в новое состояние. По сути, REST — не протокол и не стандарт, а подход, архитектурный стиль проектирования API.
Каковы принципы REST?
- Клиент-серверная архитектура — без этого REST немыслим.
- Любые данные — ресурс.
- Любой ресурс имеет ID, по которому можно получить данные.
- Ресурсы могут быть связаны между собой — для этого в составе ответа передается либо ID, либо, как чаще рекомендуется, ссылка. Но я пока не дошел до того, чтобы все было настолько хорошо, чтобы можно было легко использовать ссылки.
- Используются стандартные методы HTTP (GET, POST, PUT, DELETE) — т. к. они уже заложены в составе протокола, мы их можем использовать для того, чтобы построить каркас взаимодействия с нашим сервером.
- Сервер не хранит состояние — это значит, сервер не отделяет один вызов от другого, не сохраняет все сессии в памяти. Если у вас есть какое-либо масштабируемое облако, какая-то ферма из серверов, которая реализует ваш сервис, нет необходимости обеспечивать согласованность состояния этих сервисов между всеми узлами, которые у вас есть. Это сильно упрощает масштабирование — при добавлении еще одного узла все прекрасно работает.
Чем REST хорош?
- Он очень прост!
- Мы переиспользуем существующие стандарты, которые в ходу уже очень давно и применяются на многих устройствах.
- REST основывается на HTTP => доступны все плюшки:
- Кэширование.
- Масштабирование.
- Минимум накладных расходов.
- Стандартные коды ошибок.
- Очень хорошая распространенность (даже IoT-устройства уже умеют работать на HTTP).
Лучшие решения (независимые от технологий)
Какие в современном мире есть лучшие решения, не связанные с конкретной реализацией? Эти решения советую использовать обязательно:
- SSL повсюду — самое важное в вашем сервисе, т. к. без SSL авторизация и аутентификация бессмысленны.
- Документация и версионность сервиса — с первого дня работы.
- Методы POST и PUT должны возвращать обратно объект, который они изменили или создали, — это позволит сократить время обращения к сервису вдвое.
- Поддержка фильтрации, сортировки и постраничного вывода — очень желательно, чтобы это было стандартно и работало «из коробки».
- Поддержка MediaType. MediaType — способ сказать серверу, в каком формате вы хотите получить содержимое. Если вы возьмете какую-либо стандартную реализацию web API и зайдете туда из браузера, API отдаст вам XML, а если зайдете через какой-нибудь Postman, он вернет JSON.
- Prettyprint & gzip. Не минимизируйте запросы и не делайте компакт для JSON (того ответа, который придет от сервера). Накладные расходы на prettyprint —единицы процентов, что видно, если посмотреть, сколько занимают табы по отношению к общему размеру сообщения. Если вы уберете табы и будете присылать все в одну строку, запаритесь с отладкой. Что касается gzip, он дает выигрыш в разы. Т. ч. очень советую использовать и prettyprint, и gzip.
- Используйте только стандартный механизм кэширования (ETag) и Last-Modified (дата последнего изменения) — этих двух параметров серверу достаточно, чтобы клиент понял, что содержимое не требует обновления. Придумывать что-то свое тут не имеет смысла.
- Всегда используйте стандартные коды ошибок HTTP. Иначе вам однажды придется кому-нибудь объяснять, почему вы решили, что ошибку 419 в вашем проекте клиенту нужно трактовать именно так, как вы почему-то придумали. Это неудобно и некрасиво — за это клиент вам спасибо не скажет!
Свойства HTTP-методов

Сегодня мы будем говорить только про GET, POST, PUT, DELETE.
Если говорить вкратце об остальных, представленных в таблице, OPTIONS — получение настроек безопасности, HEAD — получение заголовков без тела сообщения, PATCH — частичное изменение содержимого.
Как вы видите, все методы, кроме POST, представленные в таблице, идемпотентны. Идемпотентность — возможность выполнить одно и то же обращение к сервису несколько раз, при этом ответ каждый раз будет одинаковым. Другими словами, не важно, по какой причине и сколько раз вы выполнили это действие. Допустим, вы выполняли действие по изменению объекта (PUT), и вам пришла ошибка. Вы не знаете, что ее вызвало и в какой момент, вы не знаете, изменился объект или нет. Но, благодаря идемпотентности, вы гарантированно можете выполнить этой действие еще раз, т. ч. клиенты могут быть спокойны за целостность своих данных.
“Safe” же значит, что обращение к серверу не изменяет содержимое. Так, GET может быть вызван много раз, но он не изменит никакого содержимого. Если бы он изменял содержимое, в силу того, что GET может быть закэширован, вам пришлось бы бороться с кэшированием, изобретать какие-нибудь хитрые параметры.
Часть 2. Практика
Выбираем технологию
Теперь, когда мы поняли, как работает REST, можем приступить к написанию RESTful API ¬ сервиса, отвечающего принципам REST. Начнем с выбора технологии.
Первый вариант — WCF Services. Все, кто работал с этой технологией, обычно возвращаться к ней больше не хотят — у нее есть серьезные недостатки и мало плюсов:
– webHttpBinding only (а зачем тогда остальные?..).
– Поддерживаются только HTTP Get & POST (и все).
+ Разные форматы XML, JSON, ATOM.
Второй вариант — Web API. В этом случае плюсы очевидны:
+ Очень простой.
+ Открытый исходный код.
+ Все возможности HTTP.
+ Все возможности MVC.
+ Легкий.
+ Тоже поддерживает кучу форматов.
Естественно, мы выбираем Web API. Теперь выберем подходящий хостинг для Web API.
Выбираем хостинг для Web API
Тут есть достаточно вариантов:
- ASP.NET MVC (старый добрый).
- Azure (облачная структура).
- OWIN — Open Web Interface for .NET (свежая разработка от Microsoft).
- IIS
- Self-hosted
OWI
OWIN — не платформа и не библиотека, а спецификация, которая устраняет сильную связанность веб-приложения с реализацией сервера. Она позволяет запускать приложения на любой платформе, поддерживающей OWIN, без изменений. На самом деле, спецификация очень проста — это просто «словарь» из параметров и их значений. Базовые параметры определены в спецификации.
OWIN сводится к очень простой конструкции:

По схеме мы видим, что есть хост, на котором есть сервер, который поддерживает очень простой «словарь», состоящий из перечня «параметр — значение». Все модули, которые подключаются к этому серверу, конфигурируются именно так. Сервер, поддерживающий этот контракт, привязанный к определенной платформе, умеет распознавать все эти параметры и инициализировать инфраструктуру соответствующим образом. Получается, что, если вы пишете сервис, который работает с OWIN, можете свободно, без изменений кода, переносить его между платформами и использовать то же самое на других ОС.
Katana — реализация OWIN от Microsoft. Она позволяет размещать OWIN-сборки в IIS. Вот так она выглядит, очень просто:
[assembly: OwinStartup(typeof (Startup))]
namespace RestApiDemo
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
}
Вы указываете, какой класс является у вас Startup. Это простой dll, который поднимается IIS. Вызывается конфигуратор. Этого кода достаточно, чтобы все заработало.
Проектируем интерфейс
Теперь спроектируем интерфейс и посмотрим, как все должно выглядеть и каким правилам соответствовать. Все ресурсы в REST — существительные, то, что можно пощупать и потрогать.
Как пример возьмем простую модель с расписанием движения поездов на станциях. Вот примеры простейших запросов REST:
- Корневые (независимые) сущности API:
- GET /stations — получить все вокзалы.
- GET /stations/123 — получить информацию по вокзалу с ID = 123.
- GET /trains — расписание всех поездов.
- Зависимые (от корневой) сущности:
- GET /stations/555/departures — поезда, уходящие с вокзала 555.
Далее я еще расскажу про DDD, почему мы делаем именно так.
Контроллер
Итак, у нас есть станции, и теперь нам нужно написать простейший контроллер:
[RoutePrefix("stations")]
public class RailwayStationsController : ApiController
{
[HttpGet]
[Route]
public IEnumerable<RailwayStationModel> GetAll()
{
return testData;
}
RailwayStationModel[] testData = /*initialization here*/
}
Это роутинг, построенный на атрибутах. Здесь мы указываем имя контроллера и просим отдать список (в данном случае — случайные тестовые данные).
OData (www.odata.org)
Теперь представьте, что у вас больше данных, чем нужно на клиенте (больше ста тащить вряд ли имеет смысл). При этом писать самому какое-либо разбиение на страницы, конечно, совсем не хочется. Вместо этого есть простой способ — использовать легкую версию OData, которая поддерживается Web API.
[RoutePrefix("stations")]
public class RailwayStationsController : ApiController
{
[HttpGet]
[Route]
[EnableQuery]
public IQueryable<RailwayStationModel> GetAll()
{
return testData.AsQueryable();
}
RailwayStationModel[] testData = /*initialization here*/
}
IQueryable позволяет вам использовать несколько простых, но эффективных механизмов фильтрации и управления данными на клиентской стороне. Единственное, что нужно сделать, — подключить OData-сборку из NuGet, указать EnableQuery и возвращать интерфейс iQueryable.
Основное отличие такой облегченной верси от полноценной в том, что здесь нет контроллера, который возвращает метаданные. Полноценная OData немного изменяет ответ (заворачивает в спец. Обертку модель, которую вы собираетесь возвращать) и умеет возвращать связанное дерево объектов, которые вы хотите ей отдать. Также облегченная версия OData не умеет делать штуки вроде join, count и т. д.
Параметры запросов
А вот что можно делать:

- $filter — фильтр, по имени, например. Все функции можно посмотреть на сайте OData — они очень помогают и позволяют существенно ограничить выборку.
- $select — очень важная штука. Если у вас большая коллекция и все объекты толстые, но при этом вам нужно сформировать какой-то dropdown, в котором нет ничего, кроме ID и имени, которое вы хотите отобразить, — поможет эта функция, которая упростит и ускорит взаимодействие с сервером.
- $orderby — сортировка.
- $top и $skip — ограничение по выборкам.
Этого достаточно, чтобы самому не изобретать велосипеда. Все это умеет стандартная JS-библиотека вроде Breeze.
EnableQuery Attribute
На самом деле OData — такая штука, которой очень легко можно выстрелить себе в ногу. Если у вас в таблице миллионы записей, и вам нужно тянуть их с сервера на клиент, это будет тяжело, а если таких запросов будет много — совсем смертельно.
Именно для таких случаев у атрибута EnableQuery (см. код выше) есть такой набор параметров, с помощью которых очень многое можно ограничить: не давать больше строк, чем надо, не давать делать join, арифметические операции и т. д. При этом писать самому ничего не надо.
- AllowedArithmeticOperators
- AllowedFunctions
- AllowedLogicalOperators
- AllowedOrderByProperties
- AllowedQueryOptions
- EnableConstantParameterization
- EnsureStableOrdering
- HandleNullPropagation
- MaxAnyAllExpressionDepth
- MaxExpansionDepth
- MaxNodeCount
- MaxOrderByNodeCount
- MaxSkip
- MaxTop
- PageSize
Зависимый контроллер
Итак, вот примеры простейших запросов REST:
- GET /stations – получить все вокзалы
- GET /trains – расписание всех поездов
- GET /stations/555/arrivals
- GET /stations/555/departures
Допустим, у нас есть вокзал 555, и мы хотим получить все его отправления и прибытия. Очевидно, что здесь должна использоваться сущность, которая зависит от сущности вокзала. Но как это сделать в контроллерах? Если мы все это будет делать роутинг-атрибутами и складывать в один класс, понятно, что в таком примере, как у нас, проблем нет. Но если у вас будет десяток вложенных сущностей и глубина будет расти еще дальше, все это вырастет в неподдерживаемый формат.
И тут есть простое решение — в роутинг-атрибутах в контроллерах можно делать переменные:
[RoutePrefix("stations/{station}/departures")]
public class TrainsFromController : TrainsController
{
[HttpGet]
[Route]
[EnableQuery]
public IQueryable<TrainTripModel> GetAll(int station)
{
return GetAllTrips().Where(x => x.OriginRailwayStationId == station);
}
}
Соответственно, все зависимые сущности выносите в отдельный контроллер. Сколько их — совершенно неважно, так как они живут отдельно. С точки зрения Web API, они будут восприниматься разными контроллерами — сама система как бы не знает, что они зависимы, несмотря на то, что в URL они выглядят таковыми.
Единственное, возникает проблема — здесь у нас “stations”, и до этого был “stations”. Если вы в одном месте что-то поменяете, а в другом — ничего не поменяете, ничего работать не будет. Однако тут есть простое решение — использование констант для роутинга:
public static class TrainsFromControllerRoutes
{
public const string BasePrefix =
RailwayStationsControllerRoutes.BasePrefix +
"/{station:int}/departures";
public const string GetById = "{id:int}";
}
Тогда зависимый контроллер будет выглядеть так:
[RoutePrefix(TrainsFromControllerRoutes.BasePrefix)]
public class TrainsFromController : TrainsController
{
[HttpGet]
[Route]
[EnableQuery]
public IQueryable<TrainTripModel> GetAll(int station)
{
return GetAll().Where(x => x.OriginRailwayStationId == station);
}
}
Вы можете делать для зависимых контроллеров простейшие операции — вы просто берете и вычисляете роут сами, и тогда вы не ошибетесь. Кроме того, эти штуки полезно использовать в тестировании. Если вы хотите написать тест и потом хотите этим управлять, а не бегать каждый раз по всем миллионам ваших тестов и исправлять все строки, где указаны эти относительные URL’ы, то вы также можете использовать эти константы. Когда вы их меняете, данные у вас меняются везде. Это очень удобно.
CRUD
Итак, мы с вами обсудили, как могут выглядеть простейшие GET-операции. Все понимают, как сделать единичный GET. Но, кроме него, нам нужно обсудить еще три операции.
- POST – создать новую сущность
- POST /Stations – JSON-описание сущности целиком. Действие добавляет новую сущность в коллекцию.
- Возвращает созданную сущность (во-первых, чтобы не было двойных походов к серверу, во-вторых, чтобы, если это нужно, вернуть со стороны сервера параметры, которые посчитались в этом объекте и нужны вам на клиенте).
- PUT — изменить сущность
- PUT /Stations/12 — Изменить сущность с ID = 12. JSON, который придет в параметре, будет записан поверх.
- Возвращает измененную сущность. Путь, который был применен много раз, должен приводить систему к одному и тому же состоянию.
- DELETE
- DELETE /Stations/12 — удалить сущность с ID = 12.
Еще примеры CRUD:
- POST /Stations — добавляем вокзал.
- POST /Stations/1/Departures — добавляем информацию об отправлении с вокзала 1.
- DELETE /Stations/1/Departures/14 — удаляем запись об отправлении с вокзала 1.
- GET /Stations/33/Departures/10/Tickets — список проданных билетов для отправления 10 с вокзала 33.
Важно понимать, что узлы — обязательно какие-то сущности, то, что можно «потрогать» (билет, поезд, факт отправления поезда и т. д.).
Антишаблоны
А вот примеры, как делать не надо:
- GET /Stations/?op=departure&train=11
Здесь query string используется не только для передачи данных, но и для действий. - GET /Stations/DeleteAll
Это реальный пример из жизни. Тут мы делаем GET на этот адрес, и он, по идее, должен удалить все сущности из коллекции — в итоге он ведет себя очень непредсказуемо из-за кэширования. - POST /GetUserActivity
На самом деле здесь GET, который записан как POST. POST нужен был из-за параметров запроса в body, но в body у GET нельзя ничего передать — GET можно передать только в query string. GET даже по стандарту не поддерживает body. - POST /Stations/Create
Здесь действие указано в составе URL — это избыточно.
Проектируем API
Допустим, у вас есть API, который вы хотите предложить людям, и есть доменная модель. Как связаны сущности API с доменной моделью? Да никак они не связаны, на самом деле. В этом нет никакой необходимости: то, что вы делаете в API, никак не связано с вашей внутренней доменной моделью.
Может возникнуть вопрос, как проектировать API, если это не CRUD? Для этого мы записываем любые действия как команды на изменения. Мы делаем сохранение, чтение, удаление команды, GET, проверку статуса этой команды. GET из коллекции команд — вы получаете список всех команд, которые вы отправляли для какой-либо конкретной сущности.
Доменная модель
Мы поговорим о связи доменной модели с объектами. В примере у нас есть отель (Hotel), есть бронирования (Reservation), комнаты (Room) и устройства (Device), к ним привязанные. В нашем проекте это позволяло управлять комнатами посредством этих устройств.

Но вот незадача: устройство — сущность, которая живет своей жизнью, и непонятно, как ее отделять от отеля. Но разделить отель и устройства, на самом деле, очень просто — поможет DDD. В первую очередь, нужно разобраться, где границы доменных областей и где границы сущностей, ответственных за согласованность системы.
Bounded context (BC)
Bounded context (изолированный поддомен) — фактически, наборы объектов, не зависимые друг от друга и имеющие совершенно независимые модели (разные). В примере мы можем взять и растащить отели и устройства на два разных BC — они не связаны между собой, но присутствует дублирование. Возникает дополнительная сущность (AttachedDevice):

Тут у нас разные представления одного и того же устройства, и в этом нет ничего страшного.
В DDD aggregate route — сущность, которая владеет всеми потомками. Это вершина нашего дерева (Hotel); то, за что можно вытянуть все остальное. А AttachedDevice так взять нельзя — его не существует, и он не имеет никакого смысла. Так же и классы Room и Reservation не имеют никакого смысла, будучи оторванными от Hotel. Поэтому доступ ко всем этим классам — исключительно через рутовую сущность, через Hotel, в данном случае. Device же — другой route с самого начала, другое дерево с другим набором полей.
Итак, если вы понимаете, что одна сущность у вас играет в двух разных доменах, просто распилите ее — и это будет всего лишь проекция мастер-сущности. В AttachedDevice будут, например, поля с номером комнаты, а в Device такие поля не нужны.
А вот примеры запросов, как они могут выглядеть в такой доменной модели:
- PUT /hotels/555/rooms/105/attachedDevices — заменить всю коллекцию привязанных устройств на новую.
- POST /hotels/555/rooms/105/attachedDevices — привязать еще одно устройство.
- DELETE /hotels/12 — удалить описание отеля с ID=12.
- POST /hotels/123/reservations — создать новую резервацию в отеле ID=123.
CQRS — Command Query Responsibility Segregation
Я не буду сейчас рассказывать про это архитектуру, но хочу коротко обрисовать, в чем ее принцип действия. Архитектура CQRS основана на разделении потоков данных.

У нас есть один поток, через который пользователь отправляет на сервер команду об изменении домена. Однако не факт, что изменение действительно произойдет, — пользователь не оперирует данными непосредственно. Итак, после того как пользователь посылает команду на изменение сущности, сервер ее обрабатывает и перекладывает в какую-то модель, которая оптимизирована на чтение — UI считывает это.
Такой подход позволит вам следовать принципам REST очень легко. Если есть команда, значит, есть сущность «список команд».
REST without PUT
В простом CRUD-мире PUT — это штука, которая меняет объекты. Но если мы строго следуем принципу CQRS и делаем все через команды, PUT у нас пропадает, т. к. мы не можем менять объекты. Вместо этого можем лишь послать объекту команду на изменение. При этом можно отслеживать статус выполнения, отменять команды (DELETE), легко хранить историю изменений, а пользователь ничего не изменяет, а просто сообщает о намерениях.
Парадигма REST without PUT — пока еще спорная и не до конца проверенная, но для каких-то случаев действительно хорошо применима.
Fine-grained VS coarse-grained
Представьте, что вы делаете большой сервис, большой объект. Тут у вас есть два подхода: fine-grained API и coarse-grained API («мелкозернистый» и «крупнозернистый» API).
Fine-grained API:
- Много маленьких объектов.
- Бизнес-логика уходит на сторону клиента.
- Нужно знать, как связаны объекты.
Сoarse-grained API:
- Создаете больше сущностей.
- Сложно делать локальные изменения, например
- POST /blogs/{id}/likes.
- Нужно отслеживать состояние на клиенте.
- Большие объекты нельзя сохранить частично.
Для начала советую проектировать fine-grained API: каждый раз, когда вы создаете объект, отправляете его на сервер. На каждое действие на стороне клиента происходит обращение к серверу. Однако с маленькими сущностями работать проще, чем с большими: если вы напишете большую сущность, вам трудно будет потом ее распилить, трудно будет делать небольшие изменения и выдергивать из нее независимые куски. Т. ч. лучше начинать с маленьких сущностей и постепенно их укрупнять.
Нумерация версий
Так уж сложилось, что к контрактам у нас в отрасли очень расслабленное отношение. Почему-то люди считают, что, если они взяли и сделали API, это их API, с которым они могут делать что угодно. Но это не так. Если вы когда-то написали API и отдали его хоть одному контрагенту, все — это версия 1.0. Любые изменения теперь должны приводить к изменению версии. Ведь люди будут привязывать свой код к той версии, которую вы им предоставили.
На прошлом проекте приходилось несколько раз откатывать API назад просто потому, что он был отдан клиенту — мы поменяли коды ошибок, но клиент уже успел привыкнуть к старым кодам.
Какие известны на текущий момент варианты нумерации версий Web API?

Самое простое — указать версию в URL.
Вот готовые варианты, когда самому ничего делать не надо:
- aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/NamespaceControllerSelector
- aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/RoutingConstraintsSample
- www.strathweb.com/2015/10/global-route-prefixes-with-attribute-routing-in-asp-net-web-api
- github.com/climax-media/climax-web-http
Библиотека Climax.Web.Http
Вот один интересный готовый вариант.
Это всего лишь роутинг атрибутов с constraint — если вы делали какие-либо серьезные объекты, наверняка делали constraint. По номеру версии в этом атрибуте ребята просто реализовали constraint. Соответственно, на один и тот же атрибут с разными версиями, но одинаковым именем контроллера вешаете на два разных класса и указываете разные версии. Все работает «из коробки….
VersionedRoute("v2/values", Version = 2)]<br>
<br>
config.ConfigureVersioning(<br>
versioningHeaderName: "version", vesioningMediaTypes: null);<br>
<br>
config.ConfigureVersioning(<br>
versioningHeaderName: null, <br>
vesioningMediaTypes: new [] { "application/vnd.model"});<source lang="cs">
<h6><b>Документация</b></h6>
Есть чудесная open-source-штука, имеющая множество различных применений - Swagger. Мы ее используем со специальным адаптером — Swashbuckle.
<ul>
<li>http://swagger.io/ </li>
<li>https://github.com/domaindrivendev/Swashbuckle</li>
</ul>
Swashbuckle:
<source lang="cs">httpConfiguration
.EnableSwagger(c => c.SingleApiVersion("v1", ”Demo API")) .EnableSwaggerUi();
public static void RegisterSwagger(this HttpConfiguration config)
{
config.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "DotNextRZD.PublicAPI")
.Description("DotNextRZD Public API")
.TermsOfService("Terms and conditions")
.Contact(cc => cc
.Name("Vyacheslav Mikhaylov")
.Url("http://www.dotnextrzd.com")
.Email("vmikhaylov@dataart.com"))
.License(lc => lc.Name("License").Url("http://tempuri.org/license"));
c.IncludeXmlComme
nts(GetXmlCommentFile());
c.GroupActionsBy(GetControllerGroupingKey);
c.OrderActionGroupsBy(new CustomActionNameComparer());
c.CustomProvider(p => new CustomSwaggerProvider(config, p));
})
.EnableSwaggerUi(
c =>
{
c.InjectStylesheet(Assembly.GetExecutingAssembly(),
"DotNextRZD.PublicApi.Swagger.Styles.SwaggerCustom.css");
});
}
}
Как видите, Swagger вытащил все, что у нас есть, вытащил XML-комментарии.

Ниже — полное описание модели GET. Если нажать на кнопку, он ее в самом деле выполнит и вернет результат.

А вот так выглядит документация к POST, первая часть:

Вот вторая часть:

Все, что было написано в XML-комментариях, — здесь.
Источники
- www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
- www.strathweb.com/2015/10/global-route-prefixes-with-attribute-routing-in-asp-net-web-api
- www.thoughtworks.com/insights/blog/rest-api-design-resource-modeling
- jacobian.org/writing/rest-worst-practices
- piwik.org/blog/2008/01/how-to-design-an-api-best-practises-concepts-technical-aspects
- www.toptal.com/api-developers/5-golden-rules-for-designing-a-great-web-api
- www.odata.org
- owin.org
- pietschsoft.com/post/2014/06/15/cqrs-command-query-responsibility-segregation-design-pattern
- blog.pivotal.io/pivotal-labs/labs/api-versioning
Приветствую друзья! В этой статье будет краткий мануал по проектированию интерфейса rest api, рассмотрим частые ошибки и используемые стандарты.
Поехали!
Версия api
Чтобы в дальнейшем не было проблем с поддержкой, рекомендуется разделять api по версиям.
Сделать это можно добавив название мажорной версии в url
/api/v1/users/1
/api/v2/users/1
...
Лучше использовать версионирование api, чем не использовать
Правила именования роутов
Не используем слэш в конце url
Ко многим разделам вашего api могут идти дополнительные атрибуты, и не всегда интуитивно понятно что их можно добавить если на конце стоит закрывающий слэш
/api/v1/posts
/api/v1/posts/ // не верно
/api/v1/posts/categories
/api/v1/posts/categories/ // не верно
/api/v1/posts?offset=12
/api/v1/posts/?offset=12 // не верно
Именование во множественном числе
Старайтесь всегда называть разделы api во множественном числе, так как существуют слова употребляемые только во множественном числе (glasses, money, clothes), и чтобы в дальнейшем не было путаницы, лучше давать имена в одном стиле
/api/v1/posts
/api/v1/post // не верно
/api/v1/post/categories // не верно
/api/v1/posts/category // не верно
/api/v1/posts/categories
Выборка по id
Чтобы получить например, данные заказа, в api должно быть достаточно передать его id
/api/orders/(( id ))
/api/orders/1
Логическая цепочка
Для того чтобы api было интуитивно понятным, необходимо соблюдать логическую цепочку наименования разделов. К примеру нам нужно получить категории поста, логично сделать так ‘Все посты -> конкретный пост -> категории’
/api/v1/posts/1/categories
/api/v1/categories/1 // не верно
/api/v1/categories?post=1 // не верно
Параметры запросов
Как правило параметры задаются в формате get в конце url. К примеру нам нужно получить только пять записей
/api/v1/posts?limit=5
/api/v1/posts/limit/5 // не верно
CRUD — create read update delete
С построением get(получить) запросов мы разобрались, теперь возмемся за создание, изменение, удаление данных. Здесь одним url не обойтись, нам нужны типы запросов POST, PUT, DELETE
Создание — post
Создание записей как правило производится POST запросом на необходимый раздел api, никаких /create/ /build/ /add/ в роуте прописывать не нужно, здесь идет фильтрация типа запроса
К примеру чтобы создать пост, нам нужно отправить POST запрос на подобный url
/api/v1/posts
/api/v1/posts/create // не верно
Чтение — GET
Стандартный get запрос на получение данных
Изменение — PUT
Как и в случае с созданием, url нам править не нужно, нужно только отфильтровать запрос по типу. Для изменения данных используется тип запроса PUT. Вот пример url для изменения поста запросом PUT
/api/v1/posts/1
Удаление — DELETE
Аналогично предыдущим, удаление производится только изменением типа запроса на DELETE
Все действия производятся только с помощью изменения запроса.
Никогда не используйте глаголы в url
Ответ сервера
Сервер всегда должен возвращать http коды, вот стандартный список ответов сервера которые обязательно нужно реализовать
- 200: Done, it was okay. // Обычно GET возвращает этот код.
- 201: “Done, and created.” // Обычно POST возвращает этот код.
- 204: “Done, and no body.” // Обычно DELETE возвращает этот код.
- 400: “Client sent me junk, and I’m not going to mess with it.”
- 401: “Unauthorized, the client should authenticate first.”
- 403: “Not allowed. You can’t have it because you logged in but don’t have permission to this thing or to delete this thing.”
- 404: “Can’t find it.”
- 410: “Marked as deleted.”
- 451: “The government made me not show it.”
Пример хорошего api блога
+--------+----------------------------------------+------------------------------------------------------+
| | POST | login |
| | POST | logout |
| | POST | password-recovery |
| | | |
| | GET | posts?offset={n}&limit={n} |
| | POST | posts |
| | PUT | posts |
| | DELETE | posts |
| | GET | posts/{n}/ |
| | GET | posts/{n}/categories |
| | | |
| | GET | categories?offset={n}&limit={n} |
| | POST | categories |
| | PUT | categories |
| | DELETE | categories |
+--------+----------------------------------------+------------------------------------------------------+
Here at RestCase we are working with many companies and help them develop secure services and APIs. When working with developers on how to design and build quality APIs and microservices and I am seeing many common design problems that are not organization / company-specific. These problems are better to be addressed at the design phase since it will make your API consistent, more readable and robust.
Here are the top 5 issues I am seeing when it comes to designing an API:
Problem 1: Using body in GET requests
In GET requests, add parameters inside the body, instead of in a URL.
Many servers might not even look at the body of the request when it comes to GET method, since the RFC7231 states that:
A payload within a GET request message has no defined semantics;
sending a payload body on a GET request might cause some existing
implementations to reject the request.
You are using HTTP GET method for «writing» while your URIs do not identify a resource but some operation you would like to perform on the resource. HTTP GET has been designed to be idempotent and safe, meaning that no matter how many times you call GET on the same resource, the response should always be the same and no change in application state should occur.
Roy Fielding’s comment about including a body with a GET request:
Yes. In other words, any HTTP request message is allowed to contain a message body, and thus must parse messages with that in mind. Server semantics for GET, however, are restricted such that a body, if any, has no semantic meaning to the request. The requirements on parsing are separate from the requirements on method semantics.
So, yes, you can send a body with GET, and no, it is never useful to do so.
This is part of the layered design of HTTP/1.1 that will become clear again once the spec is partitioned (work in progress).
Problem 2: Usage of wrong or lengthy names for parameters
Some developers still think that it is better to give a precise parameter name that states the resource, like project_id. When working with REST APIs, try to follow the best practices for URI design.
projectid -> id
projectname -> name
err_msg -> message
Since the URI already should state what is the resource, like project, there is no need for the project_id, but only id.
Also, if you are using lengthy names like «pagenum», first better use a dash(-), instead of an underline(««) like «page-num» and secondly, try to avoid it! Just add a description of the parameter in your documentation:
page | defines the page number to search.
Problem 3: Define own error code and error message.
Error Handling
Error handling in many APIs is implemented in the following way: all requests return a response code of 200, even in case of an error. This means that there is no way to distinguish between a successful and unsuccessful response other than parsing the body and checking for the presence of error or error code fields. There is a «problem details response»
which is well-defined by the RFC7807 standard that actually defines the interface of a correct and well-known error response. Unfortunately, it is not used too much since not many developers are aware of it. I wrote about it in [blog post and really recommend everyone to read it.

Error Codes
Please, consider not using this approach of returning a response code 200 (success) when something went wrong unless it is the standard way to go in your API framework. It is a good practice to make use of standard HTTP error codes, which are recognized by most clients and developers.
It makes life easier if the API client could know upfront whether to parse the body or not and how to parse it (as a data object or error object). In cases where errors are application-specific, returning a 400 (Bad request) or 500 (server error) with an appropriate error message in the response body is preferred.
Whichever error handling strategy is chosen for a given API, just make sure it is consistent and according to the widely adopted HTTP standards. This would make our lives easier.
Try to reuse the HTTP protocol first 🙂
Problem 4: Ignoring caching.
It is easy to ignore the caching by including a header «Cache-control: no-cache» in responses of your API calls. HTTP defines a powerful caching mechanism that includes ETag header, If-Modified-Since header, and 304 Not Modified response code. They allow your clients and servers to negotiate always a fresh copy of the resource and through caching or proxy servers increase your application’s scalability and performance.

ETag (entity tag) response header provides a mechanism to cache unchanged resources. Its value is an identifier that represents a specific version of the resource.
How it works?
Followings are the generally high-level steps where response header ‘ETag’ along with conditional request header ‘If-None-Match’ is used to cache the resource copy in the client browser:
-
The server receives a normal HTTP request for a particular resource, say project with id=123 to get the project details.
-
The server prepares the response but in order to help the browser with caching (By default all browsers always cache the resources (specification) so no special header in the response is needed) and includes the header ‘ETag’ with its value in the response:
ETag: «version1» -
The server sends the response with the above header, the content of project 123 in the body and with the status code 200. The browser renders the resource and at the same time caches the resource copy along with header information.
-
Later the same browser makes another request for the same resource project 123 but with following conditional request header:
If-None-Match: «version1» -
On receiving the request for project 123 along with ‘If-None-Match’ header, the server logic checks whether project 123 needs a new copy of the resource by comparing the current value of the ETag identifier generated on the content of project 123 (or saved in some other place, which is useful when the content is very big in order not to calculate the ETag again) and the one which is received in the request header.
-
If the request’s If-None-Match is the same as the currently generated/assigned value of ETag on the server, then status code 304 (Not Modified) with the empty body is sent back and the browser that uses a cached copy of project 123.
-
If the request’s If-None-Match value doesn’t match the currently generated/assigned value of ETag (say «version2») for project 123 then the server sends back the new content in the body along with status code 200. The ‘ETag’ header with the new value is also included in the response. The browser uses the new project 123 and updates its cache with the new data.
This is very useful and gives several advantages like saving network bandwidth since this the server does not return content body but relies on the browser for that. This also speeds up the communication between client and server and eventually increases your application security, scalability, and performance.
Problem 5: Returning too much data and long response times.
When you start building your REST API, don’t forget that the resources you are returning can increase both in the count and in size. With time, this can cause your microservices to be under increased load and increase the response times of your REST API.
Nobody wants to call an API and get a response after 2 minutes.
Start designing your REST APIs with support for pagination, sorting and filtering.
Filtering
URL parameters are the easiest way to add basic filtering to REST APIs. If you have an /projects endpoint which lists school projects, you can filter via the property name such as GET /projects?state=active or GET /projects?state=active&student=1234. However, this only works for exact matches. What if you want to do a range such as an age or a date range?
The problem is URL parameters only have a key and a value but filters are composed of three components:
The property or field name
The operator such as eq, lte, gte
The filter value

There are various ways to encode three components into URL param key/values.
List all the possible options for filtering in your API documentation and enforce strong validation on the inputs like checking if this is a valid number, valid date and etc…
Most endpoints that return a list of entities will need to have some sort of pagination.
Without pagination, a simple search could return millions or even billions of hits causing extraneous network traffic.
Paging requires an implied ordering. By default, this may be the item’s unique identifier but can be other ordered fields such as a created date.
Limit/Page Paging would look like GET /projects?limit=20&page=5. This query would return the 20 rows starting with the 100th row (5 pages of 20 items). Not sending the page, will default it to 0.
Sorting
Like filtering, sorting is an important feature for any API endpoint that returns a lot of data. If you’re returning a list of users, your API users may want to sort by last modified date or by email.
To enable sorting, many APIs add a sort or sort_by URL parameter that can take a field name as the value.
However, good API designs give the flexibility to specify ascending or descending order. Like filters, specifying the order requires encoding three components into a key/value pair.
Editor’s note: We’ve got an updated eBook on the topics covered in this blog series: Web API Design: The Missing Link.
In previous discussions about pragmatic REST API design, I talked about simplyfing associations, using the HTTP ? to hide complexities and optional parameters, choosing plural nouns and concrete names, and more.
What about errors in the context of RESTful API best practices? Many software developers, including myself, don’t always like to think about exceptions and error handling but it is a very important piece of the puzzle for any software developer, and especially for API designers.
Why is good error design especially important for API designers?
Bottom line, it’s about making your APIs intuitive and making developers successful.
First, developers learn to write code through errors. The «test-first» concepts of the extreme programming model and the more recent «test driven development» models represent a body of best practices that have evolved because this is such an important and natural way for developers to work.
From the perspective of the developer consuming your Web API, everything at the other side of that interface is a black box. Errors therefore become a key tool providing context and visibility into how to use an API.
Secondly, in addition to when they’re developing their applications, developers depend on well-designed errors at the critical times when they are troubleshooting and resolving issues after the applications they’ve built using your APIs are in the hands of their users.
How to think about errors in a pragmatic way with REST?
Let’s take a look at how three top APIs approach it.

Facebook
No matter what happens on a Facebook request, you get back the 200 status code — everything is OK. Many error messages also push down into the HTTP response. Here they also throw an #803 error but with no information about what #803 is or how to react to it.
Twilio
Twilio does a great job aligning errors with HTTP status codes. Like Facebook, they provide a more granular error message but with a link that takes you to the documentation. Community commenting and discussion on the documentation helps to build a body of information and adds context for developers experiencing these errors.
SimpleGeo
Provides error codes but with no additional value in the payload.
A couple of best practices
Use HTTP status codes
Use HTTP status codes and try to map them cleanly to relevant standard-based codes.
There are over 70 HTTP status codes. However, most developers don’t have all 70 memorized. So if you choose status codes that are not very common you will force application developers away from building their apps and over to wikipedia to figure out what you’re trying to tell them.
Therefore, most API providers use a small subset. For example, the Google GData API uses only 10 status codes, Netflix uses 9, and Digg, only 8.

How many status codes should you use for your API?
When you boil it down, there are really only 3 outcomes in the interaction between an app and an API:
- Everything worked
- The application did something wrong
- The API did something wrong
Start by using the following 3 codes. If you need more, add them. But you shouldn’t go beyond 8.
- 200 — OK
- 404 — Not Found
- 500 — Internal Server Error
If you’re not comfortable reducing all your error conditions to these 3, try picking among these additional 5:
- 201 — Created
- 304 — Not Modified
- 400 — Bad Request
- 401 — Unauthorized
- 403 — Forbidden
(Check out this good Wikipedia entry for all HTTP Status codes.)
It is important that the code that is returned can be consumed and acted upon by the application’s business logic — for example, in an if-then-else, or a case statement.
Make messages returned in the payload as verbose as possible

Be verbose.
Use plain language descriptions.
Add as many hints as your API team can think of about what’s causing an error.
I highly recommend you add a link in your description to more information, like Twilio does.
- API Management
- Apigee
- Google Cloud
- Developers & Practitioners
13 декабря, 2016
Технические статьи
Введение
Здравствуйте, дорогие читатели! Прежде чем вы начнёте читать эту статью, я хотел бы описать цели её создания и рассказать, что побудило меня на её написание.
На одном из проектов нашей компании появилось необходимость спроектировать серверное приложение в стиле REST. Изначально нам казалось, что это довольно простая задача и для её решения нам хватит только собственного опыта.

Но, когда стартовал процесс разработки архитектуры и приведения REST-сервисов к единому стилю, у нас с коллегами начали возникать спорные вопросы и появляться разные точки зрения на реализацию того или иного аспекта. Здесь мы поняли, что необходимо открыть гугл и обратиться к помощи коллективного разума, изучить предлагаемые лучшие практики, которые необходимо использовать при проектировании RESTful приложения.
Данная статья будет полезна для тех людей, которые уже имеют некоторый опыт работы с веб-приложениями (и, возможно, с REST-сервисами), но нуждаются в закреплении и стандартизации полученных знаний.
Определение
Для начала нужно определиться, что же такое REST. Википедия даёт на этот вопрос следующий ответ. REST (Representational State Transfer — «передача состояния представления») — архитектурный стиль взаимодействия компонентов распределённого приложения в сети. REST представляет собой согласованный набор ограничений, учитываемых при проектировании распределённой гипермедиа-системы.
Своими словами я бы объяснил понятие REST как «набор рекомендаций, который позволяет унифицировать взаимодействие клиентских и серверных приложений».
В данной статье я постараюсь рассказать об этих самых «рекомендациях», которые помогут проектировать и создавать REST-сервисы согласно общепринятым практикам.
Также нужно понимать, что такое REST-сервис. Я бы дал определение REST-сервису, как «точка взаимодействия клиентского приложения с сервером». Говоря Java терминологией — это сервлет, на который клиент посылает запрос.
Проблематика
Но прежде чем начинать описывать правила, я хотел бы донести мысль о том, что REST — это не стандарт, потому нет единых строгих правил, которых стоит придерживаться. Это значит, что до сих пор нет полной согласованности о том, какие решения лучше применять в той или иной ситуации. Очень часто заходят споры о том, какие HTTP методы использовать и какой HTTP код возвращать в каждой конкретной ситуации.
Более подробно о проблемах REST можно почитать здесь.
Соответственно, рекомендации, которые я опишу ниже, стоит принимать во внимание, но не следовать им на 100%, если это может повредить какому-либо другому аспекту проекта.
Однако если вы будете следовать всем рекомендациям, то получится спроектировать очень понятную и поддерживаемую систему, с которой приятно работать.
Название сервиса
Для начала необходимо выбрать имя для REST сервиса. Под именем сервиса я подразумеваю его путь в URI запросе. Например, http://my-site.by/api/rest/service/name. Для выбора имени нам нужно понимать, что такое «ресурсы» в архитектуре REST.
Представление ресурса
В терминологии REST что угодно может быть ресурсом — HTML-документ, изображение, информация о конкретном пользователе и т.д. Если ресурс представляет собой некоторый объект, его легко представить, используя некоторый стандартный формат, например, XML или JSON. Далее сервер может отправить данный ресурс, используя выбранный формат, а клиент сможет работать с полученным от сервера ресурсом, используя этот же формат.
Пример представления ресурса «профиль» в формате JSON:
{
"id":1,
"name":"Mahesh",
"login":"manesh"
}
REST не накладывает явных ограничений на формат, который должен быть использован для представления ресурсов, но есть ряд правил, которым нужно следовать при разработке формата, который будет использоваться для представления ресурса:
- Клиент и сервер должны «понимать» и иметь возможность работать с выбранным форматом.
- Ресурс можно полностью описать, используя выбранный формат независимо от сложности ресурса.
- Формат должен предусматривать возможность представления связей между ресурсами.
Пример представления ресурса «заказ» и его связи с ресурсом «профиль»:
{
id: 11254,
currency: "EUR",
amount: 100,
profile: {
id: 11,
uri: "http://MyService/Profiles/11"
}
}
Как видно, не обязательно полностью дублировать всю структуру ресурса, на который ссылается другой ресурс. Вместо этого можно использовать понятную ссылку на другой ресурс.
Обращение к ресурсу
Каждый ресурс должен быть уникально обозначен постоянным идентификатором. «Постоянный» означает, что идентификатор не изменится за время обмена данными, и даже когда изменится состояние ресурса. Если ресурсу присваивается другой идентификатор, сервер должен сообщить клиенту, что запрос был неудачным и дать ссылку на новый адрес. Каждый ресурс однозначно определяется URL. Это значит, что URL по сути является первичным ключом для единицы данных. То есть, например, вторая книга с книжной полки будет иметь вид /books/2, а 41 страница в этой книге — /books/2/pages/41. Отсюда и получается строго заданный формат. Причём совершенно не имеет значения, в каком формате находятся данные по адресу /books/2/pages/41 – это может быть и HTML, и отсканированная копия в виде jpeg-файла, и документ Word.
Рекомендуется при определении имени REST-сервиса использовать имена ресурсов во множественном числе. Такой подход позволяет добавлять новые REST-сервисы, лишь расширяя имена уже существующих. Например, сервис /books вернёт нам список всех книг, /books/3 вернёт информацию о 3-ей книге, а сервис /books/3/pages вернёт все страницы 3-ей книги.
Для сервисов, которые выполняют какие-то специфические действия над ресурсом, есть 2 подхода для указания действия: в имени сервиса или в его параметрах. Например, /books/3/clean или /books/3?clean. Я предпочитаю первый вариант, так как обычно такие сервисы не редко используют POST методы, которые не поддерживают передачу параметров в URl, что делает сервис, на мой взгляд, не очень читабельным. Используя определение типа действия в имени сервиса, мы делаем наш сервис более расширяемым, так как он не зависит от типа HTTP метода.
Также очень не рекомендуется использовать имена, включающие в себя несколько слов и описывающие бизнес составляющую сервиса (как это рекомендуется делать при именовании java методов). Например, вместо /getAllCars лучше сделать метод /cars. Если же метод нельзя никак описать одним словом, то необходимо применять единый стиль разделителей, я обычно использую ‘-’, что является наиболее популярным подходом. Например, /cars/3/can-sold.
Более подробно о проектировании названий REST-сервисов можно прочитать в этой статье.
HTTP методы
Далее нам необходимо выбрать HTTP метод, который будет использовать наш REST-сервис, ведь имея даже одинаковое имя, но разные методы, REST-сервисы выполняют совершенно различные действия.
В REST используются 4 основных HTTP метода: GET, POST, PUT, DELETE. В большинстве случаев каждый из методов служит для выполнения предопределённого ему действия из CRUD (create, read, update, delete — «создание, чтение, обновление, удаление»).
POST — create, GET — read, PUT — update, DELETE — delete.
ВАЖНОЕ ДОПОЛНЕНИЕ: Существуют так называемые 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 используют для создания, а PUT для редактирования, и чуть позже я объясню почему.
Приведу несколько примеров использования различных методов для взаимодействия с ресурсами.
- GET /books/ – получает список всех книг. Как правило, это упрощённый список, т.е. содержащий только поля идентификатора и названия объекта, без остальных данных.
- GET /books/{id} – получает полную информацию о книге.
-
POST /books/ – создаёт новую книгу. Данные передаются в теле запроса.
PUT /books/{id} – изменяет данные о книге с идентификатором {id}, возможно, заменяет их. Данные также передаются в теле запроса. - OPTIONS /books – получает список поддерживаемых операций для указанного ресурса (практически не используется).
- DELETE /books/{id}– удаляет данные с идентификатором {id}.
Безопасность и идемпотентность
Очень помогут в выборе HTTP метода знания о безопасности и идемпотентности этих методов.
Безопасный запрос — это запрос, который не меняет состояние приложения.
Идемпотентный запрос — это запрос, эффект которого от многократного выполнения равен эффекту от однократного выполнения.

Судя по данной таблице, GET-запрос не должен менять состояние ресурса, к которому применяется. PUT и DELETE запросы могут менять состояние ресурса, но их можно спокойно повторять, если нет уверенности, что предыдущий запрос выполнился. В принципе, это логично: если многократно повторять запрос удаления или замены определённого ресурса, то результатом будет удаление или замена ресурса. Но POST запрос, как мы видим из таблицы, небезопасный и неидемпотентный. То есть мало того что он меняет состояние ресурса, так и многократное его повторение будет производить эффект, зависимый от количества повторений. Ему по смыслу соответствует операция добавления новых элементов в БД: выполнили запрос Х раз, и в БД добавилось Х элементов.
Также приведу пример того, почему GET-запросы не должны изменять состояние ресурса. GET-запросы могут кэшироваться, например, на уровне прокси-сервера. В таком случае запрос может даже не дойти до сервера приложения, а в качестве ответа прокси-сервер вернёт информацию из кэша.
HTTP коды
В стандарте HTTP описано более 70 статус кодов. Хорошим тоном является использование хотя бы основных.
- 200 – OK – успешный запрос. Если клиентом были запрошены какие-либо данные, то они находятся в заголовке и/или теле сообщения.
- 201 – OK – в результате успешного выполнения запроса был создан новый ресурс.
- 204 – OK – ресурс успешно удалён.
- 304 – Not Modified – клиент может использовать данные из кэша.
- 400 – Bad Request – запрос невалидный или не может быть обработан.
- 401 – Unauthorized – запрос требует аутентификации пользователя.
- 403 – Forbidden – сервер понял запрос, но отказывается его обработать или доступ запрещён.
- 404 – Not found – ресурс не найден.
- 500 – Internal Server Error – разработчики API должны стараться избежать таких ошибок.
Эти ошибки должны быть отловлены в глобальном catch-блоке, залогированы, но они не должны быть возвращены в ответе.
Чем обширнее набор кодов, который мы будем использовать, тем более понятный будет API, который мы создаём. Однако нужно учесть, что некоторые коды браузеры обрабатывают по-разному. Например, некоторые браузеры получив код ответа 307 сразу же выполняют редирект, а некоторые позволяют обработать такую ситуацию и отменить действие. Прежде чем использовать тот или иной код, необходимо полностью понимать, как он будет обрабатываться на клиентской стороне!
Более подробно о HTTP кодах можно почитать здесь.
Headers
Рекомендуется при проектировании REST-сервисов явно указывать заголовки, в которых обозначен формат обмена данными:
- Content-Type — формат запроса;
- Accept — список форматов ответа.
Параметры поиска ресурсов
Чтобы упростить использование сервисов, отвечающих за возвращение какой-либо информации, и вдобавок сделать их наиболее производительными, необходимо использовать в качестве параметров запроса параметры для сортировки, фильтрации, выбора полей и пагинации.
Фильтрация
Используйте уникальный параметр запроса для каждого поля, чтобы реализовать фильтрацию. Это позволит ограничить количество выводимой информации, что оптимизирует время обработки запроса.
Например, чтобы вывести все красные книги необходимо выполнить запрос:
GET /books?color=red
Сортировка
Сортировка реализуется подобно фильтрации. Например, чтобы вывести все книги, отсортированные по году публикации по убыванию и по названию по возрастанию, нужно выполнить следующий запрос:
GET /books?sort=-year,+name
Пагинация
Для того чтобы поддержать возможность загрузки списка ресурсов, которые должны отображаться на определённой странице приложения, в REST API должен быть предусмотрен функционал пагинации. Реализуется он с помощью знакомых нам по SQL параметрам limit и offset. Например:
GET /books?offset=10&limit=5
Помимо того, хорошим тоном является вывод ссылок на предыдущую, следующую, первую и последнюю страницы в хидере Link. Например:
Link: <http://localhost/api/books?offset=15&limit=5>; rel=»next»,
<http://localhost/api/books?offset=50&limit=3>; rel=»last»,
<http://localhost/api/books?offset=0&limit=5>; rel=»first»,
<http://localhost/api/books?offset=5&limit=5>; rel=»prev»
Рекомендуется также возвращать общее количество ресурсов в хидере X-Total-Count.
Выбор полей ресурса
Для более удобного использования сервиса, для экономии трафика можно предоставить возможность управлять форматом вывода данных. Реализуется предоставлением возможности выбора полей ресурса, которые должен вернуть REST сервис. Например, если необходимо получить только id книг и их цвета, необходимо выполнить следующий запрос:
GET /books?fields=id,color
Хранение состояния
Одно из ограничений RESTful сервисов заключается в том, что они не должны хранить состояние клиента, от которого получают запросы.
Пример сервиса, не хранящего состояние:
Request1: GET http://MyService/Persons/1 HTTP/1.1
Request2: GET http://MyService/Persons/2 HTTP/1.1
Каждый из этих запросов может быть обработан независимо от другого.
Пример сервиса, хранящего состояние:
Request1: GET http://MyService/Persons/1 HTTP/1.1
Request2: GET http://MyService/NextPerson HTTP/1.1
Чтобы обработать второй запрос, серверу потребуется «запомнить» id последнего человека, который был запрошен клиентом. Т.е. сервер должен «запомнить» своё текущее состояние, иначе второй запрос не может быть обработан. При проектировании сервиса следует избегать необходимости в хранении состояния, так как это имеет ряд преимуществ.
Преимущества сервиса, не хранящего состояние:
- сервис обрабатывает запросы независимо друг от друга;
- архитектура сервиса упрощается;
- не требуется дополнительных усилий для реализации сервисов с использованием протокола HTTP, который также не хранит состояния.
Недостатки сервиса, не хранящего состояние:
- клиент сам должен отвечать за передачу необходимого контекста сервису.
Версионность
Хорошим тоном является поддержка версионности REST API. Это позволит в дальнейшем легко расширять API, без обязательного внесения изменений в клиенты, которые уже пользуются им.
Имеются несколько подходов реализации версионности:
- С использованием Accept хидера. В данном случае версия API указывается в Accept — Accept:text/v2+json
- С использованием URI. В таком подходе версия API указывается прямо в URI — http://localhost/api/v2/books
- Использование кастомного хидера. Можно использовать собственный хидер, который будет отвечать только за передачу версии API — API-Version:v2
- Использование параметра запроса. Можно использовать параметр запроса для передачи версии API — /books?v=2
Каждый из представленных способов имеет право на существование, у каждого есть свои плюсы и минусы. Однако только вам решать, какой способ реализации версионности подойдёт вашему проекту.
Документация
Для удобного пользования нашими REST сервисами нужно создать хорошую и понятную документацию. Для этих целей можно использовать различные инструменты, например, Mashape или Apiary, но я рекомендую использовать Swagger.
Swagger — это технология, которая позволяет документировать REST-сервисы. Swagger поддерживает множество языков программирования и фреймворков. Плюс, Swagger предоставляет UI для просмотра документации.
Получить более подробную информацию о Swagger можно по данной ссылке.
Архивирование
Для экономии трафика рекомендуется использовать архивирование, при передаче больших данных. Это сократит время выполнения запроса. В большинстве современных фреймворков данная функция реализована по умолчанию.
Кэширование
Также для сокращения запросов к БД и увеличения быстродействия наших REST сервисов рекомендуется применить механизм кэширования. Кэширование можно настраивать как на уровне сервера, так и в самом приложении, в зависимости от ситуации.
Кэшированием можно управлять используя следующие HTTP заголовки:
- Date — дата и время создания ресурса.
- Last Modified — дата и время последнего изменения ресурса на сервере.
- Cache-Control — заголовок HTTP 1.1 используемый для управления кэшированием.
- Age — время, прошедшее с момента последнего получения ресурса, заголовок может быть добавлен промежуточным (между клиентом и сервером) компонентом (например, прокси сервер).
Рекомендации по кэшированию:
- Рекомендуется кэшировать статические ресурсы, такие как изображения, стили css, файлы javascript.
- Не рекомендуется указывать большое время жизни кэша.
- Динамическое содержимое должно кэшироваться на короткое время или не кэшироваться вообще.
Общие рекомендации
Тип представления ресурса
Хотя REST не накладывает явных ограничений на формат, который должен быть использован для представления ресурсов, наиболее популярными являются XML и JSON.
Существует множество библиотек в разных языках программирования для удобной работы с этими форматами. Несмотря на это, рекомендуется использовать именно JSON для представления ресурсов. Это более лёгкий, читабельный формат, с которым проще работать по сравнению с XML, а также проще выполнять сериализацию/десериализацию объектов в различных языках программирования.
Однако иногда для поддержки некоторых REST клиентов сервису необходимо поддерживать XML формат. В таком случае можно реализовать поддержку обоих форматов и указывать в параметре запроса, в каком формате должен быть представлен ответ.
Использование проверенных решений для авторизации
Используйте для авторизации проверенные и отработанные схемы.
Не изобретайте свой велосипед с использованием md5 подписей данных и прочего, доверьтесь профессионалам в области защиты данных. Всё уже придумано за вас: OAuth, OpenID, APIKeys.
Обработка исключений
При возникновении ошибочных ситуаций необходимо выводить отформатированную и понятную информацию. Это относится в первую очередь к статус коду в HTTP ответе. Ошибки в сервисах чаще всего относятся к двум типам:
- 4xx — ошибки клиента;
- 5xx — ошибки сервера.
В случае ошибки клиента, например, ошибки валидации какого-то из параметров запроса, в теле ответа рекомендуется передавать полезную информацию об ошибке: сообщение, описание, код (например, в формате JSON). В случае ошибки сервера отправлять дополнительную информацию в теле ответа не всегда возможно, например, в случаях, когда сервер недоступен.
Плохим тоном является вывод всего стэк трейса исключения. Рекомендуется для каждой исключительной ситуации иметь свой код. В дальнейшем при выводе информации об ошибке можно будет добавить ссылку на документацию, в которой данный код будет выступать уникальным идентификатором.
Например:
{
"code" : 1234,
"message" : "Something bad happened :(",
"description" : "More details about the error here",
“moreInfo”: “http:/localhost/api/v2/errors/1234”
}
Полезные ссылки
-
- https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
- https://medium.com/@mwaysolutions/10-best-practices-for-better-restful-api-cbe81b06f291
- https://habr.com/ru/post/144011/
- https://habr.com/ru/post/144259/
Мануал, Техническая статья, REST, RESTful, HTTP метод
Автор оригинала: Justin Albano.
1. введение
REST-это архитектура без состояния, в которой клиенты могут получать доступ к ресурсам на сервере и управлять ими. Как правило, службы REST используют HTTP для объявления набора ресурсов, которыми они управляют, и предоставляют API, который позволяет клиентам получать или изменять состояние этих ресурсов.
В этом уроке мы познакомимся с некоторыми из лучших практик обработки ошибок REST API, включая полезные подходы для предоставления пользователям соответствующей информации, примеры из крупномасштабных веб-сайтов и конкретную реализацию с использованием примера приложения Spring REST.
Дальнейшее чтение:
Обработка ошибок для ОТДЫХА с пружиной
Spring ResponseStatusException
2. Коды состояния HTTP
Когда клиент делает запрос к HTTP — серверу — и сервер успешно получает запрос – сервер должен уведомить клиента, был ли запрос успешно обработан или нет . HTTP выполняет это с помощью пяти категорий кодов состояния:
- 100-уровневый (информационный) — Сервер подтверждает запрос
- 200-уровень (Успех) — Сервер выполнил запрос, как и ожидалось
- 300-уровень (Перенаправление) — Клиенту необходимо выполнить дальнейшие действия для завершения запроса
- 400-level (Ошибка клиента) — Клиент отправил неверный запрос
- 500-level (Server error) — Серверу не удалось выполнить допустимый запрос из-за ошибки с сервером
Основываясь на коде ответа, клиент может предположить результат конкретного запроса.
3. Обработка Ошибок
Первым шагом в обработке ошибок является предоставление клиенту правильного кода состояния. Кроме того, нам может потребоваться предоставить дополнительную информацию в теле ответа.
3.1. Основные Ответы
Самый простой способ обработки ошибок-это ответить соответствующим кодом состояния .
Некоторые распространенные коды ответов включают в себя:
- 400 Плохой запрос — Клиент отправил недопустимый запрос — например, отсутствует требуемое тело запроса или параметр
- 401 Неавторизованный — Клиенту не удалось пройти аутентификацию на сервере
- 403 Forbidden — Клиент аутентифицирован, но не имеет разрешения на доступ к запрошенному ресурсу
- 404 Не найден — Запрошенный ресурс не существует
- 412 Ошибка предварительного условия — одно или несколько условий в полях заголовка запроса оцениваются как ложные.
- 500 Внутренняя ошибка сервера — На сервере произошла общая ошибка
- 503 Услуга Недоступна — Запрошенная услуга недоступна
Будучи базовыми, эти коды позволяют клиенту понять широкий характер возникшей ошибки. Например, мы знаем, если получаем ошибку 403, что у нас нет разрешений на доступ к запрошенному ресурсу.
Во многих случаях, однако, мы должны предоставить дополнительные детали в наших ответах.
500 ошибок сигнализируют о том, что при обработке запроса на сервере возникли некоторые проблемы или исключения. Как правило, эта внутренняя ошибка не является делом нашего клиента.
Следовательно, чтобы свести к минимуму такого рода ответы клиенту, мы должны усердно пытаться обрабатывать или улавливать внутренние ошибки и отвечать другими соответствующими кодами состояния, где это возможно . Например, если исключение возникает из-за того, что запрошенный ресурс не существует, мы должны выставить это как ошибку 404, а не 500.
Это не означает, что 500 никогда не должны быть возвращены, только то, что он должен использоваться для непредвиденных условий – таких как отключение службы – которые мешают серверу выполнить запрос.
3.2. Ответы На Весенние ошибки По Умолчанию
Эти принципы настолько распространены, что Spring кодифицировала их в своем механизме обработки ошибок по умолчанию .
Чтобы продемонстрировать это, предположим, что у нас есть простое приложение Spring REST , которое управляет книгами, с конечной точкой для извлечения книги по ее идентификатору:
curl -X GET -H "Accept: application/json" http://localhost:8082/spring-rest/api/book/1
Если нет книги с идентификатором 1, мы ожидаем, что наш контроллер выдаст исключение BookNotFoundException . Выполняя GET на этой конечной точке, мы видим, что это исключение было выброшено, и тело ответа:
{
"timestamp":"2019-09-16T22:14:45.624+0000",
"status":500,
"error":"Internal Server Error",
"message":"No message available",
"path":"/api/book/1"
}
Обратите внимание, что этот обработчик ошибок по умолчанию включает метку времени возникновения ошибки, код состояния HTTP, заголовок (поле error ), сообщение (по умолчанию пустое) и URL-адрес, по которому произошла ошибка.
Эти поля предоставляют клиенту или разработчику информацию, помогающую устранить проблему , а также составляют несколько полей, составляющих стандартные механизмы обработки ошибок.
Кроме того, обратите внимание, что Spring автоматически возвращает код состояния HTTP 500 при вызове нашего BookNotFoundException . Хотя некоторые API будут возвращать код состояния 500 или другие общие коды, как мы увидим с API Facebook и Twitter — для всех ошибок ради простоты лучше всего использовать наиболее конкретный код ошибки, когда это возможно .
В нашем примере мы можем добавить @ControllerAdvice так, чтобы при возникновении BookNotFoundException наш API возвращал статус 404 для обозначения Not Found вместо 500 Internal Server Error .
3.3. Более Подробные Ответы
Как видно из приведенного выше примера Spring, иногда кода состояния недостаточно, чтобы показать специфику ошибки. При необходимости мы можем использовать тело ответа, чтобы предоставить клиенту дополнительную информацию. При предоставлении подробных ответов мы должны включать:
- Error — Уникальный идентификатор ошибки
- Сообщение — Краткое читаемое человеком сообщение
- Детализация — более подробное объяснение ошибки
Например, если клиент отправляет запрос с неверными учетными данными, мы можем отправить ответ 401 с телом:
{
"error": "auth-0001",
"message": "Incorrect username and password",
"detail": "Ensure that the username and password included in the request are correct"
}
Поле error не должно совпадать с кодом ответа . Вместо этого это должен быть код ошибки, уникальный для нашего приложения. Как правило, нет никакого соглашения для поля error , ожидайте, что оно будет уникальным.
Обычно это поле содержит только буквенно-цифровые символы и соединительные символы, такие как тире или подчеркивание. Например, 0001 , auth-0001 и incorrect-user-pass являются каноническими примерами кодов ошибок.
Часть тела message обычно считается презентабельной на пользовательских интерфейсах. Поэтому мы должны перевести это название, если мы поддерживаем интернационализацию . Таким образом, если клиент отправляет запрос с заголовком Accept-Language , соответствующим французскому языку, значение title должно быть переведено на французский.
Часть detail предназначена для использования разработчиками клиентов , а не конечным пользователем , поэтому перевод не требуется.
Кроме того, мы также можем предоставить URL — адрес — например, поле help , по которому клиенты могут перейти для получения дополнительной информации:
{
"error": "auth-0001",
"message": "Incorrect username and password",
"detail": "Ensure that the username and password included in the request are correct",
"help": "https://example.com/help/error/auth-0001"
}
Иногда мы можем захотеть сообщить более чем об одной ошибке для запроса . В этом случае мы должны вернуть ошибки в список:
{
"errors": [
{
"error": "auth-0001",
"message": "Incorrect username and password",
"detail": "Ensure that the username and password included in the request are correct",
"help": "https://example.com/help/error/auth-0001"
},
...
]
}
И когда возникает единственная ошибка, мы отвечаем списком, содержащим один элемент. Обратите внимание, что ответ с несколькими ошибками может быть слишком сложным для простых приложений. Во многих случаях достаточно ответить на первую или самую значительную ошибку.
3.4. Стандартизированные Органы реагирования
В то время как большинство API REST следуют аналогичным соглашениям, специфика обычно варьируется, включая имена полей и информацию, включенную в тело ответа. Эти различия затрудняют единообразную обработку ошибок библиотеками и фреймворками.
Стремясь стандартизировать обработку ошибок REST API, |/IETF разработала RFC 7807 , который создает обобщенную схему обработки ошибок .
Эта схема состоит из пяти частей:
- тип — Идентификатор URI, который классифицирует ошибку
- заголовок — Краткое, читаемое человеком сообщение об ошибке
- status — Код ответа HTTP (необязательно)
- подробно — Читаемое человеком объяснение ошибки
- экземпляр — URI, идентифицирующий конкретное возникновение ошибки
Вместо того чтобы использовать наше пользовательское тело ответа на ошибку, мы можем преобразовать ваше тело в:
{
"type": "/errors/incorrect-user-pass",
"title": "Incorrect username or password.",
"status": 401,
"detail": "Authentication failed due to incorrect username or password.",
"instance": "/login/log/abc123"
}
Обратите внимание, что поле type классифицирует тип ошибки, в то время как instance идентифицирует конкретное возникновение ошибки аналогично классам и объектам соответственно.
Используя URI, клиенты могут следовать этим путям, чтобы найти дополнительную информацию об ошибке таким же образом, как ссылки HATEOAS могут использоваться для навигации по REST API.
Придерживаться RFC 7807 необязательно, но выгодно, если требуется однородность.
4. Примеры
Описанные выше методы являются общими для некоторых наиболее популярных API REST. В то время как конкретные имена полей или форматов могут варьироваться между сайтами, общие шаблоны почти универсальны .
4.1. Твиттер
Например, давайте отправим запрос GET без предоставления необходимых аутентификационных данных:
curl -X GET https://api.twitter.com/1.1/statuses/update.json?include_entities=true
API Twitter отвечает ошибкой со следующим телом:
{
"errors": [
{
"code":215,
"message":"Bad Authentication data."
}
]
}
Этот ответ включает в себя список, содержащий одну ошибку, с ее кодом ошибки и сообщением. В случае Twitter нет подробного сообщения, и общая ошибка — а не более конкретная ошибка 401 — используется для обозначения того, что аутентификация не удалась.
Иногда более общий код состояния проще реализовать, как мы увидим в нашем весеннем примере ниже. Это позволяет разработчикам перехватывать группы исключений и не различать код состояния, который должен быть возвращен. Однако, когда это возможно, следует использовать наиболее конкретный код состояния .
4.2. Facebook
Подобно Twitter, Facebook Graph REST API также включает подробную информацию в свои ответы.
Например, давайте выполним POST-запрос для аутентификации с помощью API Facebook Graph:
curl -X GET https://graph.facebook.com/oauth/access_token?client_id=foo&client_secret=bar&grant_type=baz
Мы получаем следующую ошибку:
{
"error": {
"message": "Missing redirect_uri parameter.",
"type": "OAuthException",
"code": 191,
"fbtrace_id": "AWswcVwbcqfgrSgjG80MtqJ"
}
}
Как и Twitter, Facebook также использует общую ошибку — а не более конкретную ошибку 400-го уровня — для обозначения сбоя. В дополнение к сообщению и числовому коду Facebook также включает в себя поле type , которое классифицирует ошибку, и идентификатор трассировки ( fbtrace_id ), который действует как внутренний идентификатор поддержки .
5. Заключение
В этой статье мы рассмотрели некоторые из лучших практик обработки ошибок REST API, в том числе:
- Предоставление конкретных кодов состояния
- Включение дополнительной информации в органы реагирования
- Единообразная обработка исключений
Хотя детали обработки ошибок будут варьироваться в зависимости от приложения, эти общие принципы применимы почти ко всем API REST и должны соблюдаться, когда это возможно .
Это не только позволяет клиентам последовательно обрабатывать ошибки, но и упрощает код, который мы создаем при реализации REST API.
Код, на который ссылается эта статья, доступен на GitHub .