Создание RESTful API: пошаговая инструкция. Создание RESTful API: пошаговая инструкция Rest приложение

В данной статье я расскажу Вам о том, что такое REST API . Также мы затронем тему HTTP протокола. А также рассмотрим пример архитектурного дизайна REST API.

Немного теории

О том, что такое API, я подробно рассказывал . Напомню, что API – это некий набор правил, с помощью которых приложение или какой-либо один его компонент могут взаимодействовать, общаться, если хотите, с другим приложением или компонентом. Прикладной интерфейс программирования (API ) может возвращать данные в разных форматах, например в JSON , XML или в бинарном формате, но в REST API мы будем использовать JSON -формат, как наиболее удобный.

Давайте посмотрим на пример. Возможно, Вы уже знакомы с тем, что такое система контроля версий Git . Ее web-версия – это Github . Так вот, у Github есть собственное API , с помощью которого можно получить какую-либо полезную информацию, например о пользователях и организациях, их проектах, и т.д. Давайте взглянем на пример:

Curl https://api.github.com/orgs/Yandex

В этом примере мы используем консольную утилиту curl для того, чтобы получить данные через API . Ее можно загрузить с официального сайт проекта . Она позволяет делать все то же самое что и расширение curl в PHP , только для этого не нужно писать код, так как вся функциональность доступна посредством интерфейса командной строки. Вообще, незаменимая вещь для тестирования различных прикладных интерфейсов . Есть еще альтернатива в виде расширения для Chrome – Postman .

Данная команда вернет нам большой JSON-объект , содержащий различные данные о компании.

Теперь остановимся подробнее на том, что же такое REST . Это сокращение может быть расшифровано в следующем виде: представление данных для клиента в формате удобном для него. Очень важно запомнить, что REST – это не протокол, а подход, архитектурный стиль к написанию прикладных интерфейсов .

Если говорить еще проще то, REST – это архитектурный стиль, а RESTful API – это его практическое воплощение, и чем больше приложение отвечает критериям стиля REST , тем более оно RESTful .

RESTful API сводится к четырем базовым операциям:

  • получение данных в удобном для клиента формате
  • создание новых данных
  • обновление данных
  • удаление данных

REST функционирует поверх протокола HTTP , поэтому стоит упомянуть о его основных особенностях. Для каждой операции указанной выше используется свой собственный HTTP метод :

  • GET – получение
  • POST – создание
  • PUT – обновление, модификация
  • DELETE – удаление

Все эти методы в совокупности называют CRUD (create, read, update, delete) – (создать, прочитать, обновить, удалить) операциями.

Фактически в REST существует единственный, непротиворечивый общий интерфейс для запросов, например, к базам данных, что является его важнейшим преимуществом. На следующей картинке показано соответствие HTTP методов SQL операциям и концепции CRUD .

Т.е. HTTP метод POST соответствует SQL операции INSERT, метод GET – операции SELECT и т.д .

Для каждого HTTP запроса есть свой статус. И они нужны, чтобы грамотно с точки зрения REST API оформить ответ и отдать клиенту. Статусов много, поэтому их всех не перечислить, однако важно знать их группировку:

  • 100 – 199 – это статусы несущие информационный характер
  • 200 - 299 – статусы успешной операции
  • 300 – 399 – статусы перенаправления (редиректа)
  • 400 – 499 – статусы ошибок на стороне клиента
  • 500 – 599 – статусы ошибок на стороне сервера

Вообще, как делается API . Создается некая точка входа для запросов, api.php , например. Этому API , могут передаваться, например, такие запросы:

  • http://site.com/api.php?action=create.user&id=1&key=46syhy77sash
  • http://site.com/api.php?action=delete.user&id=1&key=46syhy77sash
  • http://site.com/api.php?action=get.user&id=1&key=46syhy77sash

где параметр

  • action – это действие, которое необходимо выполнить
  • id – идентификатор пользователя
  • кey – ключ доступа (фактически, временный пароль)

Однако этот подход, несмотря не некую простоту, не лишен недостатков, хотя бы потому, что для всех видов запросов используется один метод GET , тогда как в спецификации HTTP их определено с десяток, каждый для своей конкретной области.

Сегодня REST API используется повсюду, начиная от сайтов, заканчивая мобильными приложениями, поэтому важно знать как работать с ним, так как рано или поздно может возникнуть необходимость в создании клиента (мобильного приложения,например) для своего сайта или того же блога.

Таким образом, REST API призван создать четко структурированный подход в написании прикладных интерфейсов. Так, как с каждым днем становится все больше и больше данных, к которым необходимо открыть доступ.

Этот пост - ответ на вопрос, заданный в комментарии к одной из моих статей.

В статье я хочу рассказать, что же из себя представляют HTTP-методы GET/POST/PUT/DELETE и другие, для чего они были придуманы и как их использовать в соответствии с REST.

HTTP

Итак, что же представляет из себя один из основных протоколов интернета? Педантов отправлю к RFC2616 , а остальным расскажу по-человечески:)

Этот протокол описывает взаимодействие между двумя компьютерами (клиентом и сервером), построенное на базе сообщений, называемых запрос (Request) и ответ (Response). Каждое сообщение состоит из трех частей: стартовая строка, заголовки и тело. При этом обязательной является только стартовая строка.

Стартовые строки для запроса и ответа имеют различный формат - нам интересна только стартовая строка запроса, которая выглядит так:

METHOD URI HTTP/VERSION ,

Где METHOD - это как раз метод HTTP-запроса, URI - идентификатор ресурса, VERSION - версия протокола (на данный момент актуальна версия 1.1).

Заголовки - это набор пар имя-значение, разделенных двоеточием. В заголовках передается различная служебная информация: кодировка сообщения, название и версия браузера, адрес, с которого пришел клиент (Referrer) и так далее.

Тело сообщения - это, собственно, передаваемые данные. В ответе передаваемыми данными, как правило, является html-страница, которую запросил браузер, а в запросе, например, в теле сообщения передается содержимое файлов, загружаемых на сервер. Но как правило, тело сообщения в запросе вообще отсутствует.

Пример HTTP-взаимодействия

Рассмотрим пример.

Запрос:
GET /index.php HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9b5) Gecko/2008050509 Firefox/3.0b5 Accept: text/html Connection: close
Первая строка - это строка запроса, остальные - заголовки; тело сообщения отсутствует

Ответ:
HTTP/1.0 200 OK Server: nginx/0.6.31 Content-Language: ru Content-Type: text/html; charset=utf-8 Content-Length: 1234 Connection: close ... САМА HTML-СТРАНИЦА...

Ресурсы и методы

Вернемся к стартовой строке запроса и вспомним, что в ней присутствует такой параметр, как URI. Это расшифровывается, как Uniform Resource Identifier - единообразный идентификатор ресурса. Ресурс - это, как правило, файл на сервере (пример URI в данном случае "/styles.css"), но вообще ресурсом может являться и какой-либо абстрактный объект ("/blogs/webdev/" - указывает на блок «Веб-разработка», а не на конкретный файл).

Тип HTTP-запроса (также называемый HTTP-метод) указывает серверу на то, какое действие мы хотим произвести с ресурсом. Изначально (в начале 90-х) предполагалось, что клиент может хотеть от ресурса только одно - получить его, однако сейчас по протоколу HTTP можно создавать посты, редактировать профиль, удалять сообщения и многое другое. И эти действия сложно объединить термином «получение».

Для разграничения действий с ресурсами на уровне HTTP-методов и были придуманы следующие варианты:

  • GET - получение ресурса
  • POST - создание ресурса
  • PUT - обновление ресурса
  • DELETE - удаление ресурса
Обратите внимание на тот факт, что спецификация HTTP не обязывает сервер понимать все методы (которых на самом деле гораздо больше, чем 4) - обязателен только GET, а также не указывает серверу, что он должен делать при получении запроса с тем или иным методом. А это значит, что сервер в ответ на запрос DELETE /index.php HTTP/1.1 не обязан удалять страницу index.php на сервере, так же как на запрос GET /index.php HTTP/1.1 не обязан возвращать вам страницу index.php, он может ее удалять, например:)

В игру вступает REST

REST (REpresentational State Transfer) - это термин был введен в 2000-м году Роем Филдингом (Roy Fielding) - одним из разработчиков протокола HTTP - в качестве названия группы принципов построения веб-приложений. Вообще REST охватывает более широкую область, нежели HTTP - его можно применять и в других сетях с другими протоколами. REST описывает принципы взаимодействия клиента и сервера, основанные на понятиях «ресурса» и «глагола» (можно понимать их как подлежащее и сказуемое). В случае HTTP ресурс определяется своим URI, а глагол - это HTTP-метод.

REST предлагает отказаться от использования одинаковых URI для разных ресурсов (то есть адреса двух разных статей вроде /index.php?article_id=10 и /index.php?article_id=20 - это не REST-way) и использовать разные HTTP-методы для разных действий. То есть веб-приложение, написанное с использованием REST подхода будет удалять ресурс при обращении к нему с HTTP-методом DELETE (разумеется, это не значит, что надо давать возможность удалить всё и вся, но любой запрос на удаление в приложении должен использовать HTTP-метод DELETE).

REST дает программистам возможность писать стандартизованные и чуть более красивые веб-приложения, чем раньше. Используя REST, URI для добавления нового юзера будет не /user.php?action=create (метод GET/POST), а просто /user.php (метод строго POST).

В итоге, совместив имеющуюся спецификацию HTTP и REST-подход наконец-то обретают смысл различные HTTP-методы. GET - возвращает ресурс, POST - создает новый, PUT - обновляет существующий, DELETE - удаляет.

Проблемы?

Да, есть небольшая проблема с применением REST на практике. Проблема эта называется HTML.

PUT/DELETE запросы можно отправлять посредством XMLHttpRequest, посредством обращения к серверу «вручную» (скажем, через curl или даже через telnet), но нельзя сделать HTML-форму, отправляющую полноценный PUT/DELETE-запрос.

Дело в том, спецификация HTML не позволяет создавать формы, отправляющие данные иначе, чем через GET или POST. Поэтому для нормальной работы с другими методами приходится имитировать их искусственно. Например, в Rack (механизм, на базе которого Ruby взаимодействует с веб-сервером; с применением Rack сделаны Rails, Merb и другие Ruby-фреймворки) в форму можно добавить hidden-поле с именем "_method", а в качестве значения указать название метода (например, «PUT») - в этом случае будет отправлен POST-запрос, но Rack сможет сделать вид, что получил PUT, а не POST.

В этой статье я поделюсь опытом проектирования 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
  • WebSockets APIs
  • Library-based APIs
    • Java Script
  • Class-based APIs
    • C# 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).
  • Self-hosted
OWI
OWIN - не платформа и не библиотека, а спецификация, которая устраняет сильную связанность веб-приложения с реализацией сервера. Она позволяет запускать приложения на любой платформе, поддерживающей OWIN, без изменений. На самом деле, спецификация очень проста - это просто «словарь» из параметров и их значений. Базовые параметры определены в спецификации.

OWIN сводится к очень простой конструкции:

По схеме мы видим, что есть хост, на котором есть сервер, который поддерживает очень простой «словарь», состоящий из перечня «параметр - значение». Все модули, которые подключаются к этому серверу, конфигурируются именно так. Сервер, поддерживающий этот контракт, привязанный к определенной платформе, умеет распознавать все эти параметры и инициализировать инфраструктуру соответствующим образом. Получается, что, если вы пишете сервис, который работает с OWIN, можете свободно, без изменений кода, переносить его между платформами и использовать то же самое на других ОС.

Katana - реализация OWIN от Microsoft. Она позволяет размещать OWIN-сборки в IIS. Вот так она выглядит, очень просто:

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.
Контроллер

Итак, у нас есть станции, и теперь нам нужно написать простейший контроллер:

Public class RailwayStationsController: ApiController { public IEnumerable GetAll() { return testData; } RailwayStationModel testData = /*initialization here*/ }

Это роутинг, построенный на атрибутах. Здесь мы указываем имя контроллера и просим отдать список (в данном случае - случайные тестовые данные).

OData (www.odata.org)

Теперь представьте, что у вас больше данных, чем нужно на клиенте (больше ста тащить вряд ли имеет смысл). При этом писать самому какое-либо разбиение на страницы, конечно, совсем не хочется. Вместо этого есть простой способ - использовать легкую версию OData , которая поддерживается Web API.

Public class RailwayStationsController: ApiController { public IQueryable 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, и мы хотим получить все его отправления и прибытия. Очевидно, что здесь должна использоваться сущность, которая зависит от сущности вокзала. Но как это сделать в контроллерах? Если мы все это будет делать роутинг-атрибутами и складывать в один класс, понятно, что в таком примере, как у нас, проблем нет. Но если у вас будет десяток вложенных сущностей и глубина будет расти еще дальше, все это вырастет в неподдерживаемый формат.

И тут есть простое решение - в роутинг-атрибутах в контроллерах можно делать переменные:

Public class TrainsFromController: TrainsController { public IQueryable GetAll(int station) { return GetAllTrips().Where(x =>

Соответственно, все зависимые сущности выносите в отдельный контроллер. Сколько их - совершенно неважно, так как они живут отдельно. С точки зрения Web API, они будут восприниматься разными контроллерами - сама система как бы не знает, что они зависимы, несмотря на то, что в URL они выглядят таковыми.

Единственное, возникает проблема - здесь у нас “stations”, и до этого был “stations”. Если вы в одном месте что-то поменяете, а в другом - ничего не поменяете, ничего работать не будет. Однако тут есть простое решение - использование констант для роутинга :

Public static class TrainsFromControllerRoutes { public const string BasePrefix = RailwayStationsControllerRoutes.BasePrefix + "/{station:int}/departures"; public const string GetById = "{id:int}"; }

Тогда зависимый контроллер будет выглядеть так:

Public class TrainsFromController: TrainsController { public IQueryable 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.

Вот готовые варианты, когда самому ничего делать не надо:

Вот один интересный готовый вариант.

Это всего лишь роутинг атрибутов с constraint - если вы делали какие-либо серьезные объекты, наверняка делали constraint. По номеру версии в этом атрибуте ребята просто реализовали constraint. Соответственно, на один и тот же атрибут с разными версиями, но одинаковым именем контроллера вешаете на два разных класса и указываете разные версии. Все работает «из коробки….

VersionedRoute("v2/values", Version = 2)]
config.ConfigureVersioning(
versioningHeaderName: "version", vesioningMediaTypes: null);
config.ConfigureVersioning(
versioningHeaderName: null,
vesioningMediaTypes: new { "application/vnd.model"});

Документация
Есть чудесная open-source-штука, имеющая множество различных применений - Swagger. Мы ее используем со специальным адаптером - Swashbuckle.
  • http://swagger.io/
  • https://github.com/domaindrivendev/Swashbuckle
Swashbuckle: 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("[email protected]")) .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"); }); } }

Теги: Добавить метки

В данной заметке пример самого простого REST API на PHP без использования какого-либо фреймворка и других средств. Целью есть предоставить общую картину - как это все работает.
Недавно я уже опубликовал , в которой описан процесс создания REST API для проекта на Yii2 .

Т.к. никакие фреймворки с маршрутизаторами в примере использоваться не будут, нужно начать с перенаправления всех запросов на "точку входа" - файл index.php. Для сервера на Apache это можно сделать в файле.htaccess который должен располагаться в корне проекта:
Options +FollowSymLinks IndexIgnore */* RewriteEngine on # Перенаправление с ДОМЕН на ДОМЕН/api RewriteCond %{REQUEST_URI} ^/$ RewriteRule ^(.*)$ /api/$1 #Если URI начинается с api/ то перенаправлять все запросы на index.php RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^api/(.*)$ /index.php
Согласно правил, ссылка должна начинаться на /api и,например, для API работающего с таблицей users должна иметь такой вид:
ДОМЕН/api/users

Пример файла index.php
run(); } catch (Exception $e) { echo json_encode(Array("error" => $e->getMessage())); }
Как видно из кода - будем работать с объектом usersApi , т.е. с пользователями (таблица users). Т.к. для простоты примера я не использую тут Composer или другой механизм для автозагрузки классов, просто подключим файл класса с помощью
require_once "UsersApi.php";
Кроме пользователей, может потребоваться сделать api и для других сущностей, поэтому все классы различных API должны иметь один общий костяк, который будет определять метод запроса, действие для выполнения и тд. Создаем файл Api.php c абстрактным классом Api :
requestUri = explode("/", trim($_SERVER["REQUEST_URI"],"/")); $this->requestParams = $_REQUEST; //Определение метода запроса $this->method = $_SERVER["REQUEST_METHOD"]; if ($this->method == "POST" && array_key_exists("HTTP_X_HTTP_METHOD", $_SERVER)) { if ($_SERVER["HTTP_X_HTTP_METHOD"] == "DELETE") { $this->method = "DELETE"; } else if ($_SERVER["HTTP_X_HTTP_METHOD"] == "PUT") { $this->method = "PUT"; } else { throw new Exception("Unexpected Header"); } } } public function run() { //Первые 2 элемента массива URI должны быть "api" и название таблицы if(array_shift($this->requestUri) !== "api" || array_shift($this->requestUri) !== $this->apiName){ throw new RuntimeException("API Not Found", 404); } //Определение действия для обработки $this->action = $this->getAction(); //Если метод(действие) определен в дочернем классе API if (method_exists($this, $this->action)) { return $this->{$this->action}(); } else { throw new RuntimeException("Invalid Method", 405); } } protected function response($data, $status = 500) { header("HTTP/1.1 " . $status . " " . $this->requestStatus($status)); return json_encode($data); } private function requestStatus($code) { $status = array(200 => "OK", 404 => "Not Found", 405 => "Method Not Allowed", 500 => "Internal Server Error",); return ($status[$code])?$status[$code]:$status; } protected function getAction() { $method = $this->method; switch ($method) { case "GET": if($this->requestUri){ return "viewAction"; } else { return "indexAction"; } break; case "POST": return "createAction"; break; case "PUT": return "updateAction"; break; case "DELETE": return "deleteAction"; break; default: return null; } } abstract protected function indexAction(); abstract protected function viewAction(); abstract protected function createAction(); abstract protected function updateAction(); abstract protected function deleteAction(); }
Осталось реализовать абстрактные методы и свойство $apiName , которое уникально для каждого отдельного API. Для этого создаем файл UsersApi.php :
getConnect(); $users = Users::getAll($db); if($users){ return $this->response($users, 200); } return $this->response("Data not found", 404); } /** * Метод GET * Просмотр отдельной записи (по id) * http://ДОМЕН/users/1 * @return string */ public function viewAction() { //id должен быть первым параметром после /users/x $id = array_shift($this->requestUri); if($id){ $db = (new Db())->getConnect(); $user = Users::getById($db, $id); if($user){ return $this->response($user, 200); } } return $this->response("Data not found", 404); } /** * Метод POST * Создание новой записи * http://ДОМЕН/users + параметры запроса name, email * @return string */ public function createAction() { $name = $this->requestParams["name"] ?? ""; $email = $this->requestParams["email"] ?? ""; if($name && $email){ $db = (new Db())->getConnect(); $user = new Users($db, [ "name" => $name, "email" => $email ]); if($user = $user->saveNew()){ return $this->response("Data saved.", 200); } } return $this->response("Saving error", 500); } /** * Метод PUT * Обновление отдельной записи (по ее id) * http://ДОМЕН/users/1 + параметры запроса name, email * @return string */ public function updateAction() { $parse_url = parse_url($this->requestUri); $userId = $parse_url["path"] ?? null; $db = (new Db())->getConnect(); if(!$userId || !Users::getById($db, $userId)){ return $this->response("User with id=$userId not found", 404); } $name = $this->requestParams["name"] ?? ""; $email = $this->requestParams["email"] ?? ""; if($name && $email){ if($user = Users::update($db, $userId, $name, $email)){ return $this->response("Data updated.", 200); } } return $this->response("Update error", 400); } /** * Метод DELETE * Удаление отдельной записи (по ее id) * http://ДОМЕН/users/1 * @return string */ public function deleteAction() { $parse_url = parse_url($this->requestUri); $userId = $parse_url["path"] ?? null; $db = (new Db())->getConnect(); if(!$userId || !Users::getById($db, $userId)){ return $this->response("User with id=$userId not found", 404); } if(Users::deleteById($db, $userId)){ return $this->response("Data deleted.", 200); } return $this->response("Delete error", 500); } } Методы связанные с базой данных и получением данных из нее просто для примера.

Создавая веб-сервис, нужно учитывать многое как со стороны бизнеса, так и стороны разработки. Клиент очерчивает требования — команда разработчиков их выполняет.

В свою очередь, в зависимости от требований и целей, уже на старте необходимо определиться с архитектурой проекта, так как от этого зависит то, как продукт будет функционировать после своего релиза. Сегодня мы поговорим об одной из самых популярных из них — REST.

Понять на самом базовом уровне, что такое REST и зачем он нужен, можно из статьи Райана Томайко “Как я объяснил жене, что такое REST”. Тем не менее, мы скорее подойдем к вопросу с технической стороны. Итак, приступим.

Что такое REST?

Representational state transfer (передача состояния управления) — это архитектурный стиль, применяемый при разработке веб-сервисов, и устанавливающий 6 правил для их построения.

Соблюдающие эти правила веб-сервисы называют RESTful сервисами. Помимо этого, REST также требует использование большинства возможностей протокола http.

Итак, что же это за правила?

  • Uniform Interface — единый интерфейс
  • Stateless — отсутствие состояний
  • Cacheable — кэширование
  • Client-Server — разграниченная архитектура клиент-сервер
  • Layered System — многоуровневая система
  • Code on Demand — код по запросу

Поговорим о каждом из них более подробно.

Единый интерфейс

REST архитектура подразумевает определение единого интерфейса взаимодействия всех клиентов с сервером.

При использовании REST, клиент работает с сервером получая ресурсы и управляя ими. Ресурсами при таком подходе могут выступать, например, таблицы в базе данных, хотя это и не всегда возможно, так как запросы могут иметь достаточно сложную логику взаимодействия с ресурсами.

Сам интерфейс должен разделять работу с разными ресурсами приложения по разным URL-адресам. Благодаря правильно выполненному разделению запросов, появляется возможность для лёгкого разделения одного веб-приложения на несколько, а это, в свою очередь, значительно облегчает портируемость и масштабируемость приложения.

К слову, помимо всего этого, API также становится более понятным для разработчиков даже при отсутствии детальной документации.

Также REST требует, чтобы названия ресурсов представляли собой существительные во множественном числе. Не следует перегружать систему множеством URL. Каждый ресурс в самом простом случае должен иметь только 2 URL адреса. Например:

  • Формат для получения коллекции элементов: /buildings
  • Формат для получения элемента по id: /buildings/{id}

Информация передаваемая между клиентами и сервером должна быть конвертирована в удобный для передачи формат. Примерами таких форматов может быть JSON или XML, хотя всё же наиболее популярным на текущий момент является как раз JSON.

Тем не менее, некоторые веб-сервисы умеют поддерживать работу с несколькими форматами одновременно.

В таком случае формат возвращаемых данных сервером и формат данных, который сервер должен обработать, управляется с помощью HTTP заголовков Accept и Content-type. В заголовках могут передаваться разные типы. Например: application/json или application/xml.

Отсутствие состояний

Каждый запрос в RESTful-сервисе должен уникально идентифицировать себя ресурсом или ресурсами, и оперировать их полными состояниями. REST веб-сервисы не должны хранить какие-либо данные в сессиях запросов или cookies.

Кэширование

Клиенты должны иметь возможность кешировать запросы. Таким образом, ответы явно или неявно определяют себя как кешируемые или нет, чтобы клиенты не могли использовать устаревшие или некорректные данные в ответ на дальнейшие запросы.

Правильно спроектированное кэширование позволяет уменьшить нагрузку на сервер и увеличить скорость работы клиентских приложений, что способствует большей масштабируемости и производительности.

Клиент-сервер

Интерфейс, который реализует взаимодействие клиентов и серверов, должен явно разделять обязаности клиентов и серверов и при этом должен быть реализован независимо от работы любого из них. Например:

  • клиенты не должны иметь представления о хранимой на сервере информации, так как это зона ответственности сервера.
  • Сервер не должен быть разработан в привязке к UI какого-либо клиента.

Layered system

Система может иметь промежуточные сервера и клиент не может точно знать с каким именно сервером он взаимодействует. Промежуточные сервера могут выполнять функцию балансировщиков нагрузки, кэша запросов и т.д.

Code on demand

Применение данного правила опционально. Его суть заключается в том, что сервер может передавать выполняемый на стороне клиента код Javascript, который позволяет временно расширить, или модифицировать функционал клиента.

Почему именно REST?

Несмотря на то, REST подходит для всех проектов, работающих по http, иногда его реализация может быть невозможна в силу недостаточных навыков команды. Тем не менее, если освоить REST должным образом, перед разработчиками открывается множество преимуществ:

  • Улучшенная производительность. REST возвращает исключительно данные, указанные в запросе. Таким образом быстрее обрабатываются запросы и выдаются ответы.
  • Масштабируемость. Приложение может работать на множестве серверов, что позволяет балансировать нагрузку между ними. Также сервис можно разделить на несколько “микросервисов”, которые смогут работать параллельно друг другу.
  • Портируемость благодаря единому интерфейсу
  • Прозрачность взаимодействия — за счёт своей стандартизации API остаётся понятным для пользователя.
  • Легкость изменений. Благодаря меньшей связанности кода, снижается вероятность “поломать” запросы при внесении изменений в другие части приложения.

Методы http

Http содержит 4 метода: GET, POST, PUT, DELETE. Каждый метод должен использоваться для разных целей и идентифицировать функционал, который запрос реализует.

  • GET. Используется для получения ресурсов (список ресурсов или один ресурс с указанием его id)
  • POST. используется для создания ресурсов
  • PUT. Ииспользуется для обновления ресурсов по id, который передается в URL
  • DELETE. Используется для удаления ресурсов по id, который передается в URL

Ниже мы привели таблицу, содержащуя возможные запросы для потенциального ресурса people. В ней использованны все возможные http-методы и приведены ответы, которые эти запросы могут возвращать.

Http статус-коды

При использовании REST для веб-сервисов необходимо правильно подобрать http статусы для соответсвуюших ответов сервера. Сам по себе http имеет несколько десятков статус-кодов, но мы приведём 10 наиболее часто используемых:

  1. 200 OK — хорошо
  2. 201 Created — создано
  3. 204 No Content — нет содержимого
  4. 304 Not modified — не изменялось
  5. 400 Bad request — неверный запрос
  6. 401 Unauthorized — не авторизован
  7. 403 Forbidden — запрещён
  8. 404 Not found — не найдено
  9. 409 Conflict — конфликт
  10. 500 Internal Server error — внутренняя ошибка сервера

Http статус-коды имеют большое количество кодов для обработок ошибок, но этих кодов не достаточно для любого приложения.

В подобных случаях в тело ответа можно включить сообщение, раскрывающее суть ошибки более подробно (используя json, xml и т.д.). Клиенты, которые уже знают про полученный код, смогут корректно его обработать.

Дополнительно

  • Запросы, возвращающие коллекции элементов, должны иметь возможность пагинации, сортировки, фильтрации
  • Даты и время в запросах следует передавать в формате unix timestamp в миллисекундах.
  • Версия приложения должна быть зашита в URL приложения как node. Например: api.app.com/v1/buildings

Если у вас остались какие-либо вопросы, вы можете связаться с нами для получения профессиональной технической консультации. Мы будем рады помочь вам с реализацией любых ваших идей!

Loading...Loading...