Перенос функциональности сайта, интернет-магазина или портала в мобильное приложение имеет ряд преимуществ как для владельца онлайн-сервиса, так и для его клиентов. Владелец получает дополнительный канал связи со своей целевой аудиторией и возможность персонализировать рекламные объявления, а пользователь – более удобный интерфейс, дополнительный функционал и возможность получения своевременных оповещений.

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

Существует множество фреймворков, ориентированных на разработку API. Особенно их много на NodeJS, но и на других языках – достаточно. Тем не менее, когда задача состоит в использовании существующего функционала и данных проекта, то менять его архитектуру в корне, переписывать всё на другом языке или фреймворке – нерационально. Мы пишем на своём фреймворке ZeroEngine, который ориентирован на высоконагруженные проекты и работает по принципу plug-in’ов. Кратко принцип работы ZeroEngine можно описать так: новый «модуль» можно встроить в любой уже существующий, а также перехватить управление выдачей в нужный момент.

Резюмируем вводные данные


Требуется написать REST API для сайта. Архитектура позволяет внедрить роутер и использовать существующий функционал полностью или частично.

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

ДЕЙСТВИЕ /объект/идентификатор/метод


Метод и URL должны чётко описывать выполняемую методом API функцию. Строго говоря, при именовании метода API, метод запроса (GET, POST, PUT, DELETE) является в предложении «глаголом», а адрес представляет собой «путь» от общего к частному.

Например:

  • GET /images («получить изображения») – вернёт список изображений;
  • POST /image – опубликует изображение;
  • PUT /image/123 – «положит» переданное значение в изображение номер 123.

Мы сознательно разделяем image и images, чтобы было сразу понятно, что именно придёт в ответ на запрос – массив или единичный объект.

Семантические ошибки


Для сообщения об ошибке мы не назначаем свои коды, а пользуемся стандартным набором HTTP-кодов. Чтобы упростить обработку на стороне приложения, код дублируется в теле ответа и дополняется описанием. Мы убеждены, что снабжать разработчика приложения длинным описанием всех ошибок не стоит выгоды в трафике во время отладки.

Меньше методов — меньше запросов


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

Не тестировать дважды


Разумеется, речь идёт о дублировании функциональных тестов модульными. Как и в остальных случаях, мы стараемся использовать инструменты по их назначению: юнит-тестами мы покрываем модули сайта и роутера, а сам API тестируем с помощью dredd и API Blueprint.

Разработка через документацию


Именно для этого мы используем API Blueprint и сервис apiary. Сначала мы описываем то, что хотим получить в итоге. Далее – продумываем структуру методов, их возвращаемые значения, варианты ошибок и прочее. Только после этого пишем API. Такой подход имеет множество преимуществ, и позволяет разработчикам API оперативно получать комментарии от разработчиков приложения, исключая двойную работу.

Версионность


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

Постоянная интеграция


Мы используем TeamCity, но любой CI-сервис, в том числе облачный, поддерживает unit и dredd-тесты, а также интеграцию с Apiary. При успешном тестировании мы актуализируем внешние тестовые площадки и анализируем несколько метрик. Эти действия позволяют быстро отследить возникшие проблемы и обеспечивают постоянное наличие свежей документации.

Внедрение Unit-тестирования в существующий проект


Возможно, вы уже хотите опробовать используемые нами принципы. Значит, в сети станет ещё на один хороший проект больше. Тем не менее, в процессе вы неизбежно столкнетесь с трудностями по внедрению модульных тестов в ваш проект, если до этого тесты под него не писались.

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

Мы делаем это примерно так: в папке нашего модуля мы устанавливаем PHPunit и его зависимости, а в теле самого модуля вызываем модифицированный testRunner:

$out = '';
$module = "console";
		
$testRunner = new PHPUnit_TextUI_TestRunner();
$testPrinter = new ZeroTech_printer($out);
	
$testRunner->setPrinter($testPrinter);
$testSuite = new PHPUnit_Framework_TestSuite();
		
foreach (glob(U_PATH . "/tests/*test.php") as $filename)
{
    $testSuite->addTestFile($filename);
}
$testRunner->doRun($testSuite, array("verbose" => true));	

Результат выполнения будет в переменной $out. Останется только вывести результат на экран или в шаблон.

Модуль доступен через админку и выглядит так:



Функциональные тесты с помощью dredd


Как уже было отмечено выше, для прототипирования, документирования и тестирования нашего API мы используем сервис apiary и его утилиту dredd:

  • Описываем функционал в формате API Blueprint (своеобразный markdown на стероидах): Разделяем методы на группы, описываем, зачем он нужен; какие заголовки / формат данных / параметры / атрибуты метод принимает, какие из них обязательные; какие есть ограничения; что метод возвращает; на что отвечает ошибками; в каком именно формате.
  • Сохраняем в файл и запускаем dredd file.apib.
  • Чиним проваленные тесты, рефакторим.
  • Выгружаем на apiary.

Выглядит APIB синтаксис примерно так:

 FORMAT: 1A 
# Group User 
 
## /user 
###  GET - Получение данных профиля пользователя [GET]
   + Response 200 (application/json)
   + Attributes
   + first_name: Иван (required, string) - Имя пользователя (только русские символы)        
   + last_name: Иванов (required, string) - Фамилия пользователя (только русские символы)        
   + dob: 1988-10-01 (required, string) - Дата рождения 
   + sex: 1 (required, number) - Пол (0 - женский, 1 - мужской) 
   + city: Москва (required, string) - Город         

### POST - Создание нового пользователя [POST] 
   + Request (application/json)    
   + Attributes        
   + first_name: Иван (required, string) - Имя пользователя (только русские символы)        
   + last_name: Иванов (required, string) - Фамилия пользователя (только русские символы)        
   + dob: 1988-10-01 (required, string) - Дата рождения        
   + sex: 1 (required, number) - Пол (0 - женский, 1 - мужской)        
   + city: Москва (required, string) - Город 
 
   + Response 201  
      {      
        message: “Successfully created”,      
        id: 123 
       } 

Apiary преобразует всё это в удобный интерфейс с mock-сервером. Разработчики приложения могут использовать его даже в том случае, если «живой» API ещё не написан или работает некорректно. Можно также использовать mock-сервер в качестве песочницы.



Кроме того, можно смотреть историю тестов утилитой dredd, если у вас, например, нет интерфейса непрерывной интеграции.

Заключение


В заключение хочется ещё раз заострить внимание на том, что не столь важно, какие инструменты и методики вы используете в разработке, главное, чтобы существовало понимание цели их использования. Наша задача как программистов, чтобы программы максимально точно и быстро делали то, что от них требуется. Всё остальное – вторично.
Поделиться с друзьями
-->

Комментарии (25)


  1. Fesor
    21.07.2017 19:03

    Функциональные тесты с помощью dredd

    Как вы решаете вопросы установки прекондишенов для тестов? Мы пробовали — по итогу я считаю что это бесполезная вещь, с большего потому что долго и потому что прекондишены выставлять сложно. Мы из apiblueprint генерим jsonschema и тестируем точки выхода на предмет совпадения схемы. В итоге можно получить полное покрытие всей API.


    Ну или можно заюзать graphql где схема жестко задекларирована и ее проще проверять.


    1. ValeriaVG
      21.07.2017 19:30

      В общем случае, мы описываем пары Request/Response для ситуаций, где меняется JSON-схема: например, для успешного ответа и ответов с ошибками.

      Фактически, использование именно сервиса Apiary (и формата apiblueprint, как следствие) обусловлено необходимостью иметь актуальную документацию по API с минимальной тратой ресурсов на ее поддержание.

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


      1. Fesor
        22.07.2017 10:18
        +1

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


        Но на документацию это решение не сказывается.


  1. Mehdzor
    21.07.2017 19:23

    Вы храните документацию вместе с кодом?


    1. ValeriaVG
      21.07.2017 19:32

      Да, документация разбита на несколько файлов, которые можно тестировать отдельно или вместе.
      А по коммиту в репозиторий файлы склеиваются в один и публикуются на apiary


      1. Mehdzor
        21.07.2017 19:39

        А редактируете API через что?


        1. ValeriaVG
          21.07.2017 19:57

          Через текстовый редактор, у Apiary есть web-редактор, но для объемных файлов он не очень удобен


          1. Mehdzor
            21.07.2017 20:11
            +1

            Никакими превью не пользуетесь? В Apiary больше всего бесило это, что для нормального редактирования приходится на уши вставать. И сам стандарт пока сырой аж капец как.


            1. ValeriaVG
              21.07.2017 21:51

              Нет, JSON — синтаксис разве что.

              Есть api-blueprint-preview для Atom'а, но лично не тестировала.


        1. Fesor
          22.07.2017 10:20
          +1

          блупринт это в целом просто маркдаун, я за 2 года использования не заметил необходимости использовать что-то еще. Можно просто линтер фоном запускать.


          А вообще рекомендую еще посмотреть в сторону raml1.0.


          1. ValeriaVG
            22.07.2017 12:39

            Спасибо, на первый взляд — хороший вариант, обязательно выделю время на изучение.


  1. aamonster
    22.07.2017 00:27

    Предлагаю ввести термин "default API" по аналогии с "default city". Чтобы сразу было ясно: если в заголовке упомянуто API — это исключительно для веб-приложений, всяким шудрам, пишущим не для веба, можно не заглядывать.


    1. ValeriaVG
      22.07.2017 12:37
      +2

      Признаюсь, мне и в голову не приходило что кто-то может подумать, что статья про API Arduino, к примеру, да и в анонсе статьи вроде бы недвусмысленно обозначена тематика. Возможно стоит добавить RESTful API в название?


      1. aamonster
        22.07.2017 17:53

        Да не обращайте внимания, это так, шутливое ворчание. Даже я догадался, что речь про веб — хотя в ленте на m.habrahabr.ru ничто на это не указывает (но именно потому что в любой другой области автор указал бы, о каком API идёт речь, а в вашей области разработчики зачастую забывают, что существует остальной мир).


      1. Fesor
        23.07.2017 12:05
        +1

        стоит добавить RESTful API в название?

        не стоит, ибо restful api не бывает. Можно назвать это http api и это будет честно.


        1. VolCh
          24.07.2017 10:03
          +1

          HTTP RESTish API :)


        1. ValeriaVG
          24.07.2017 10:49

          Смею считать, что программный интерфейс протокола уступает в смысловой нагрузке программному интерфейсу передачи состояний. Хотя по данным google http api действительно используется чаще нежели чем rest api.


          1. Fesor
            24.07.2017 11:00
            +1

            вся проблема в том что rest это архитектурный стиль, используемый web-ом, ключевое тут — гипертекст. И если ваши мобилки не умеют строить UI используя гипертекст — то взаимодействие клиент-серверное не может быть описано как rest. Вот и все.


            image


            1. ValeriaVG
              24.07.2017 12:17

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


              1. Fesor
                24.07.2017 12:20

                именно в этом «архитектурном стиле»,
                гипертекст в нем, в большинстве своем не используется

                Если там нет гипертекста, нет HATEOAS, значит это нельзя описать "этим" архитектурным стилем. HATEOAS является одной из основных вещей в WEB (и в REST, поскольку оно именно WEB и описывает) которая является эдакой киллер фичей, позволяющей бесконечнйы скейл в купе с URI.


                Хорошая новость — нам это и не нужно. Потому не надо подменять понятие "rest" до "http"… слишком много хороших идей таким образом размыло до того что все потеряло всякий смысл. И нет смысла пытаться впихнуть гипертекст в апишки для мобилок и т.д.


                1. ValeriaVG
                  24.07.2017 16:12

                  REST никоим образом не завязан на гипертексте, мне гораздо чаще встречались реализации с данными в формате JSON.

                  Гипертекст, формально говоря, — это текст со ссылками на другие гипертексты (википедия).

                  Гипермедиа, отвечающая за «H» в HATEOAS, в свою очередь, в любом REST сервисе используется практически всегда, так как в нее входят помимо размеченного текста еще и медийные ресурсы (фото, аудио, видео)


                  1. Fesor
                    24.07.2017 20:03
                    +1

                    REST никоим образом не завязан на гипертексте, мне гораздо чаще встречались реализации с данными в формате JSON.

                    вы читали первоисточник? REST это архитектурный стиль. Что значит "архитектурный стиль" — это совокупность ограничений накладываемых на систему. Какие у нас есть ограничения:


                    • клиент-сервер
                    • stateless
                    • cacheable (прямое следствие предыдущего)
                    • uniform interface
                      • Identification of resources
                      • manipulation of resources through representations
                      • Self-descriptive messages
                      • hypermedia as the engine of application state
                    • layered system
                    • code-on-demand (опционально)

                    Вот если у вас всего этого нет — у вас не REST. На сегодняшний день единственное что подходит под описание REST — это сам Web. Что не удивительно ибо вся эта замута с REST является лишь описанием почему Web сделан так как сделан.


                    Гипермедиа, отвечающая за «H» в HATEOAS, в свою очередь, в любом REST сервисе используется практически всегда, так как в нее входят помимо размеченного текста еще и медийные ресурсы (фото, аудио, видео)

                    вы можете добавить ссылочки в ваш json но клиент от этого не будет сам добавлять что-то на UI. Реальность такова что на клиенте хардкодится что отображать и в каком контроле. Если вы прислали картинку — ее кинут в конкретный UI контрол. В случае с JS это будет либо элемент img либо video. В любом случае конкретный вариант будет задан явно.


                    Более того, HATEOAS это декларация переходов между различными состояниями, которое декларируется элементами гипермедиа (теги a и form в html). Эти элементы скрывают действие которые надо совершить клиенту. И за счет того что у нас сервер отвечает за описание переходов мы можем скейлиться не меняя клиенты. С мобилками увы так не выйдет ибо это будет равносильно реализации браузера что точно никому не нужно. Намного проще взять какой graphql и фигачить json-ки не заморачиваясь.


                    1. ValeriaVG
                      25.07.2017 11:30

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


                      1. VolCh
                        25.07.2017 12:08
                        +1

                        Вероятно, имелось в виду, что на практике их не существуют. Никто в здравом уме не делает полноценные REST API — овчинка выделки не стоит.


                      1. Fesor
                        26.07.2017 00:16

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


                        Использовать чистый HTTP как прикладной протокол не эффективно. Именно поэтому у вас есть документация в apiblueprint с примерами поведения вашей надстройки над HTTP. И по сути мы от REST используем только те пункты которые относятся к HTTP.


                        Потому я бы просто предложил не использовать этот термин. Вообще. HTTP API вполне себе хорошо описывает суть того что нам нужно. Мы все еще можем брать идеи "ссылок" на связанные ресурсы, в угоду information hiding, но это далеко от гипертекста. Более того, лично я был удивлен что андройдщикам это "неудобно".


                        может именоваться REST API?

                        В идеале даже если мы решимся на это и реализуем подобное, тут будет конфликт терминологии. API про контракты, REST про переход из одного состояния в другое, и гипертекст как способ декларировать эти переходы. То есть мы не можем выделить контракт когда говорим о REST. У вас может быть спецификация описывающая как клиент должен реагировать на отдельные элементы гипермедиа, но это не контракт.