Когда мы написали сотое API мы поняли…

    Мы в Perfect Solutions на прошлой неделе написали сотое по счету API. За все это время, ценой граблей, костылей, велосипедов и рефакторинга, мы поняли, что выработали отличную стратегию «как писать API и прекратить боль и страдание».

    Этот пост о версировании, поддержке, багфиксинге и полном цикле жизни API.

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

    Продуктовый подход


    API — не «набор функций для всех», не «набор для решения любых задач», не «сделать и забыть».

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

    Что такое продуктовый подход к API? Это значит что API состоит из фич и багов. Фичи должны быть просты и атомарны: один метод API — воспринимается как одна фича. Фича ли, регистрация пользователя через API? Да, фича! Фича ли, отправка push-уведомления? Фича!

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

    Атомарность операций


    Если Ваше API делает в одном методе 3 INSERT в СУБД — там обязана быть транзакция. Если ее нет — это не просто будет причиной проблем, но и причиной ненайденных и невоспроизводимых из интерфейса проблем.

    И еще важный момент в понятии атормарности. Фича — атомарна. Если при регистрации нового юзера из API ему обязаны быть указаны роли (из системы прав) — значит метод API для регистрации пользователя обязан в одном запросе делать обе вещи:

    • Создавать пользователя
    • Делать assign переданного списка ролей


    Например вот такой метод, в случае yii2:
     public function actionRegister($username, $password, array $roleNames)
     {
      ...
     }
    


    Проверка всех входящих параметров и соответствие протоколам


    Как и любые данные полученные от пользователя, всё что приходит в API обязано проходить валидацию. API пользуются разработчики, но они такие же пользователи, люди.

    Стандартные http-ответы соответствующие реальности — это важный момент для упрощения поддержки и дебага. Некорректный запрос, непроходящая валидация — «400 Bad Request», Внутренняя ошибка — «500 Internal Server Error». Это очень важно для логирования (в логах nginx) и починки всех будущих багов. Если API всегда отдает статус 200 — будет очень сложно найти в логах причины ошибок.

    Вы будете чинить баги


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

    Я это говорю к тому, что как любой продукт, API — то что придется отлаживать, тестировать и поддерживать. А значит есть самый обычный набор правил:
    • Соблюдайте протокол (как минимум Status Code)
    • Логируйте все — на стороне API логируйте запросы и ответы, на стороне клиента — то же самое. Забавный момент — оно еще и отличатся может ;-)
    • Тестируйте его, делайте ревью кода и помните что API — это продукт, пользователи которого — разработчики
    • Делайте специальный параметр для дебага — чтобы на стороне клиента можно было получить не только ответ от API, но и всю отладочную информацию. Рядышком с ответом.


    Никогда не верьте в то что Вы изобрели способ версирования API



    Версирование API помогает терять обратную совместимость в новых методах, не выкидывая старые методы, оставляя их в старой версии. Интернет предлагает массу способов версировать API. Способов для разных фреймворков, подходов и всего прочего, но серебряной пули нет. За то есть магазин в который легко влезает почти любая пуля — location в nginx.
     location /api/v1/ {
        ...
     }
    
     location /api/v2/ {
        ...
     }
    

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

    Document Drive Development


    Документирование API часто является болью. Документация почти всегда не актуальна, а инструменты вроде swagger все равно требуют поддержки. Этот вопрос легко решается, если облюдать продуктовый подход — начните с документации. Вот прямо когда будете заводить задачу в вашей jira/redmine — там и опишите все что хотите от API. Прямо в формате документации.

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

    ФП, как аналогия


    Пишите простые методы API. Представьте что пишите функцию на Erlang. Приличную часть проблем несут «методы всия руси», «метод для всего» или любой метод перегруженный логикой.

    Методы API должны быть атомарны, но при этом достаточны. И слишком мелкое разбиене и обратная сторона — приведут к проблемам.

    Удачи!


    Я коротко изложил наш субъективный опыт разработки API, но не привел никакого осмысленного кода — у вас все равно будет ваш код.

    А вот все это коротко в виде тезисов:
    • Продуктовый подход
    • Атомарность операций
    • Проверка всех входящих параметров и соответствие протоколам
    • Логируйте все
    • Никогда не верьте в то что Вы изобрели способ версирования API
    • Document Drive Development
    • ФП, как аналогия
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 41
    • –2
      У меня только один вопрос. Кто тот добрый дядя, оплачивающий вам ваши бесконечные эксперименты?
      • +15
        Это я и есть. Такие эксперименты приносят хорошие деньги — можно и оплачивать.
        • 0
          Ахах) Напомнило: «Вы думаете, господа, что государство — это вы? Вы ошибаетесь, господа, государство — это я!» (Людовик XIV).
      • +2
        Жаль, что не рассказали относительно того, как именно вы версионируете API на уровне кода.
        Под каждую версию API держите свою ветку?
        Или поддерживаете все версии внутри одной кодовой базы?
        • 0
          Новое API — новый микросервис, у нас SOA
          • 0
            Т.е. новый репозиторий? А что происходит, если нужно поднять минорную версию? Например с 1.0 до 1.2. Или накатить фикс сразу на две мажорные: 1.2 -> 1.2, 2.0 -> 2.1.
            • +2
              В понятиях микросервисов и нашем подходе — минорная версия это багфикс. Он обязан быть обратносовместим и сохранять интерфейс.
              После появления новой версии, старая снимается с поддержки даже с багфиксрв.

              Обычно у нас версия 1 — это рабочий прототип на php, а 2ая это полноценная версия на C++. Далее изменения уже не требуются.

              Ну и читайте статью — https://megamozg.ru/post/24718/, там все ответы
        • +8
          «Никогда не верьте в то что Вы изобрели способ версирования API» — долгие годы ждал, когда эту правду скажут вслух.
          • 0
            Надо будет статью полную такой правды в виде тезисов написать
          • +3

            Добавлю также от себя:


            • Все методы, изменяющие состояние, будь то в БД или in-memory, должны быть как минимум POST, или же детализироваться — PUT/DELETE
            • Есть критические бизнес-функции, требующие не только некоего входного набора параметров — но и, скажем некоей проверочной контрольной суммы на ключевые поля добавляемой сущности. Скажем так, checksum token в таких методах — это очередная линия обороны против плохо составленных запросов.
            • Отдельные части location'а должны быть как можно более краткими. Скажем, для случая если вы запрашиваете информацию по товару — /products/get/{id}, а не /get-product/{id}

            Большая часть советов по обходу граблей REST API была описана еще в статье от Яндекса
            https://habrahabr.ru/company/yandex/blog/265569/

            • +6
              Отдельные части location'а должны быть как можно более краткими. Скажем, для случая если вы запрашиваете информацию по товару — /products/get/{id}, а не /get-product/{id}

              Я бы предложил GET /products/{id}
            • +1
              Мне нравится как организован REST-модуль в Yii2. Все элементарно просто и при этом функционально и соответствует стандартам и лучшим практикам. Советую посмотреть их документацию и полистать исходники, наверняка что-то новое для себя откроете: https://github.com/yiisoft/yii2/blob/master/docs/guide-ru/rest-quick-start.md

              Я даже делал клиентсткую REST-библиотеку для Qt (доступен здесь https://github.com/kafeg/qtrest), которая основана на философии Yii2 REST API.
              • 0

                Как там реализовано версионирование?

                • 0
                  В рамках проекта API создается модуль по названию версии v1, v2, etc…
                  В каждом модуле — набор контроллеров, которые используют единый набор моделей данных.

                  При создании новой версии API — наследуем все контроллеры от контроллеров предыдущей версии, внося необходимые изменения в каждый новый контроллер.

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

                  Например: https://github.com/githubjeka/yii2-rest/blob/master/rest/versions/v2/controllers/PostController.php
                  • 0

                    Это делается автоматом, или надо делать руками? Что делать, если я не хочу, чтобы у разных версий были одинаковые DTO?

                    • 0
                      1. Наследование руками, но можно набросать пару скриптов.
                      2. Сгенерируйте новую модель.
                      • 0
                        Наследование руками, но можно набросать пару скриптов.

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

                        • +7
                          Ну приехали.

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

                          Ну ок, давайте разберемся, почему Ваша статья неверна с моей точки зрения.

                          Во первых в разработке Yii/Django и других фреймворков участвуют далеко не идиоты. Во вторых, я набил немало шишек при разработке и использовании API на разных языках, с обоих сторон и имею представление, какое API могут предоставить заказчики.

                          А во вторых Вы хвастаетесь что написали уже 100-е API, но при этом:
                          — пишете о валидации. На дворе 2016 год. Что у вас за фреймворк что не умеет валидацию на уровне моделей? А если умеет — то зачем валидация в API? Вы что не описываете правила валидации в каждой модели, а выносите это в API? А потом дублируете, если в проекте кроме API еще есть и Web-морда? А почему тогда Вы не пишете о других вещах, которыми занимаются модели? Например поведения (behaviour)?
                          — пишете о логировании. Так и чего, какие параметры запросов/ответов логировать? Если не иметь представления и прочитать лишь вашу статью — то наверное «url, тело запроса, тело ответа, код ответа сервера». А как же заголовки запросов/ответов, дата и время запроса, user-agent устройства? Вы же 100 API написали, ни разу не понадобилось? А почему вы не написали о том что и как логировать при загрузке файлов на сервер? А как версионировать логи и удалять старые? А нужны ли уровни логирования и как их использовать? У Вас есть практики на эти случаи?
                          — далее, версионарование АПИ и тезис из Вашего коммента. " как минимум версионирование делается так, как этого хочет разработчик". Я не понял что вы хотите сказать то? Разработчик имеет полную свободу — хочешь копируй или наследуй контроллеры руками, а не хочешь — напиши простой скрипт на Bash который это сделает за тебя.
                          — Вы сделали 100 API, но после Вашей статьи в комментариях люди делают замечание — НЕ ИСПОЛЬЗУЙТЕ GET МЕТОД ДЛЯ ИЗМЕНЕНИЯ СОСТОЯНИЯ СЕРВЕРА. Да йопт, это чуть ли не самый главный тезис который нужно понять в жизни при разработке API чего угодно. А Вы этого не поняли? Или не захотели рассказать?
                          — К 100-му API Вы поняли, какой формат данных лучше использовать — JSON или XML? Да? Почему не написали? Нет? Да как нет, Вы же 100 API уже написали!
                          — документация. Фактически, Вы говорите прямо: пишите документацию руками, не используйте Swagger. Ок, предложите, как тестировать и читать доку к API, не имея Swagger? Внешними инструментами? То есть мне нужно писать документацию в системе управления проектами, копировать ее периодически в сорцы и затем еще использовать сторонние приблуды для тестирования API? ну и де тут продуктивность? А вот пример — доку веду я, как менеджер проекта. Девелопер мне скидывает список кодов ответа сервера на один из методов, я их заношу в доку. Через день девелопер меняет коды ответов, и… забывает сообщить мне/у меня запара/сообщение etc… Дока бац и неактуальна! Или я все же внес его правки в доку, а потом бац и коммит в гите откатил другой, более опытный девелопер, которому не понравился код на код-ревью — опять дока неактуальна. Ну и шо делать? Запомните — документация API должна всегда следовать за кодом и по мере возможности, всегда генерироваться автоматически, после каждого очередного билда. А если там HATEOAS, то о документации вообзе можно не думать, ибо все по стандарту.
                          — пагинация. О да! Это тоже одна из интереснейших вещей. Вы ни разу не использовали ее? Или еще не поняли как правильно? Где совет? Нахрен в интернете статья, которая говорит «мы написали 100 апи, мы ВСЕЕЕЕ поняли!!!» и в которой ни слова о пейджинге данных? А между тем их существует аж несколько видов, почитайте на досуге: http://www.django-rest-framework.org/api-guide/pagination/
                          — сортировка, фильтрация данных. Где? Как лучше? Посоветуйте? Не вижу!!!
                          — «Соблюдайте протокол (как минимум Status Code)». И что? Ну где хоть пример протокола? Хоть за что зацепиться? Вот не знаю я например с чего вообще начать написание API, какой я протокол сам придумаю?
                          — etc…

                          Так вот.

                          1. Я считаю Вашу статью абсолютно не состоятельной и бесполезной, а Ваши тезисы и советы — непроработанными. Я посоветовал Вам ознакомиться с нормальной практикой и решил не указывать на огрехи. Но, вместо того, чтобы последовать совету или забить, Вы решили голословно меня послать. Ловите ответ.
                          2. Вы обещали методологию, выработанную на реальном опыте. А в статье лишь теория, причем плохая.

                          Дискуссию продолжать не буду.
                          • –2
                            Вы ознакомьтесь с фреймворком для начала, а затем делайте выводы [...] Ну ок, давайте разберемся, почему Ваша статья неверна с моей точки зрения.

                            Вы для начала посмотрите, с кем вы переписываетесь, а затем делайте выводы.


                            далее, версионарование АПИ и тезис из Вашего коммента. " как минимум версионирование делается так, как этого хочет разработчик". Я не понял что вы хотите сказать то?

                            Я хочу сказать очень простую вещь: в Yii2 нет — или я не нашел по вашему описанию — никакой реализации версионирования. Просто нет такой концепции. Следовательно, Yii2 не определяет, как именно будет сделано версионирование, за это отвечает разработчик. А, следовательно, какой шаблон разработчик выберет, такой и получится.


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

                            С этого места, пожалуйста, поподробнее — по какому именно стандарту?

                            • +1
                              в Yii2 нет — или я не нашел по вашему описанию — никакой реализации версионирования.

                              Так и есть. Разработчику предлагается сделать то, что вы описали в своем комментарии, используя стандартные концепции — то есть модуль вместо версии.

                            • –1
                              бомбануло
                              • 0
                                Валидация состояния модели и валидация http запроса — разные валидации. Совпадать они могут разве что в примитивных CRUD приложениях.
                  • +5
                    Насчет документации — мы пошли другим путем. Документация содержится непосредственно в коде, реализующем методы REST API. У нас Scala, это несколько улучшает внешний вид метаданных и уменьшает избыточность. При старте приложения по этим метаданным генерится HTML и вывешивается на /api в каждом микросервисе. Таким образом:
                    — документация всегда актуальна
                    — документация всегда под рукой (если ты можешь обращаться к микросервису, ты можешь сделать GET /api в браузере)
                    — и, в результате, мнгновенный ответ на вопросы типа «а что вообще умеет эта штука, которую мы два года назад написали?» и «подзабыл название поля в API, как быстро посмотреть»
                    • 0
                      Если в вашем случае это удобно — то супер. Я больше про общий случай — у нас php и C++
                      • 0
                        Ну т.е. для себя мы сделали переносимый метод не зависящий от инструментов
                      • 0
                        Используете что-то стандартное или свое? Можно ссылку или простой пример кода метода с документацией (настоящий текст можно заменить на что-то обезличенное)?
                        Спасибо!
                        • 0

                          Мы начинали со swagger, но в итоге от него отказались — его спецификация до сих пор реализована только частично и качество инструментария, мягко говоря, не очень хорошее.


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


                            summary("API call summary")
                            description("long, possibly multiline description of this API call"
                            pathParam[String]("db", "current database name")
                            pathParam[String]("coll", "current collection")
                            queryParam[Int]("offset", "current paging offset")
                            queryParam[Int]("limit", "number of elements per page")
                            produces[MyResponseTO]("application/javascript")
                            get("/db/{db}/coll/{coll}", (db, coll) => { ..... Future successful response })
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • 0
                          Документация почти всегда не актуальна, а инструменты вроде swagger все равно требуют поддержки.

                          Вот тут можно чуть подробнее? О какой именно поддержке Сваггера вы говорите?

                          • 0
                            Как минимум написать 3 буквы в разметке. Это тоже поддержка. И дело не в том сколько букв, а в том что работают Люди — и мы все время от времени забываем их поправить.
                            А решение с постановкой задачи в виде доки — исключает любые факторы и не завязано на инструменты.
                            • 0

                              Нет, подождите. Я не знаю, о какой разметке вы говорите (у нас, например, это просто часть апи: нельзя выкатить незадокументированный метод), но дело даже не в этом. В любом случае же нужны какие-то усилия на документирование. И если я правильно понял (а я не исключаю, что понял неправильно), то вы предлагаете копировать документацию из одного места в другое («Выложите новый метод на боевой сервер — просто скопируете документацию из задачи в ваше хранилище документации.»). И теперь у вас два источника одной информации. Проблема в том, что документацию часто приходится дополнять, поправлять. Теперь на каждую правку нужно делать ещё и копирование, о котором забыть гораздо проще.


                              В целом то, что вы описываете, больше похоже на «сначала досканально спроектируйте метод апи, включая техническое проектирование, а затем — реализуйте». То есть, на этапе проектирования вы уже его документируете. Это всё правильно, да. Но что мешает затем документацию закрепить за методом и не копипастить повсюду? Сваггер и ему подобные инструменты как раз позволяют это сделать.

                              • 0
                                Видимо вы поняли частично правильно.

                                Документация к методу API — это же не только его название и набор параметров. Как минимум описание «что? зачем? когда нельзя использовать? когда нужно?». Ведь иначе новый человек пришедший в проект, через 3 года (когда старых людей уже нет в команде) — просто ничего не поймет из доки и придется читать код. Есть еще важный момент — аналитики и менеджеры тоже должны знать что там есть и понимать это человеческим языком — иначе каждая их задача в итоге превратится в ежедневную «допилку» API и они не смогут экономить время разработки.

                                Два источника — совсем не так. Задача на разработку API в jira/redmine — ну никак не источник документации ее вообще потом могут не найти, да и искать не нужно. Источник один — где там удобно в wiki, в конфлюенс или где то еще.

                                Проблема в том, что документацию часто приходится дополнять, поправлять.

                                Если так приходится делать — вы либо теряете обратную совместимость, либо делаете новую версию, либо изначально сделали не то что хотели — это совсем не вопрос ведения документации, скорее вопрос к тому что вы делаете и зачем.
                                • 0
                                  Как минимум описание «что? зачем? когда нельзя использовать? когда нужно?».

                                  Конечно, именно о такой документации я и говорю. Держим в коде, экспортируем куда угодно при помощи всяких клёвых утилит, многократно упомянутых выше.


                                  … Выложите новый метод на боевой сервер — просто скопируете документацию из задачи в ваше хранилище документации
                                  … Задача на разработку API в jira/redmine — ну никак не источник документации ее вообще потом могут не найти, да и искать не нужно.

                                  Вы начинаете путаться в показаниях.


                                  Если так приходится делать — вы либо теряете обратную совместимость, либо делаете новую версию...

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

                                  • 0
                                    забыли описать важную фичу, описали что-то недостаточно ясно, да просто орфографические и пунктуационные ошибки.


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

                                    Вы начинаете путаться в показаниях.

                                    Не начинаю. Вы снова не поняли. В задаче есть «документация» — да. но ее там никто кроме разработчика читать не будет. читать ее будут в хранилище документации. вы часто ставите задачи? в теле задачи пишите что нужно сделать и доку. ок. задача готова и идет в топку т.к. никому не нужна. документация идет в хранилище. все проще, чем вы представляете. не усложняйте ;-)
                          • –1
                            На мой взгляд, данной «статье» место в твитере. Вопрос API ну совсем не раскрыт… demo версия что ли?
                            • –1
                              Twitter вам доплачивает?
                              • 0
                                Это был сарказм, на тему объёма статьи, ибо в тв ограничение на длину сообщения.
                                Я не против данной публикации, вы многое написали правильно, но еще больше упустили.
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • +1
                                Террабайты не нужны — логротейт же. А от io спасает буферизация.
                                • +1
                                  А анализировать удобно — zgrep, grep, cut, и всяким таким, если случай не особо тяжелый.
                                  • 0

                                    А правильно логгировать — отдельное искусство. Для высоконагруженных серверов обычно применяют вот такие практики:


                                    • Успешный запрос должен логгировать аккуратно выверенный минимум информации. Если объем реально большой, то логгировать нужно каждый n-ный запрос. Тогда в случае массовых ошибок в логах гарантированно окажутся нужные данные
                                    • При высоких требованиях к latency используют не текстовые, а двоичные логи, опционально еще и сжимая их перед записью
                                    • В наше время сеть куда быстрее диска, так что если нет жестких требований к целостности логов, можно логгировать их в сеть на специализированный сервис, типа Kafka
                                    • Должна быть возможность включать debug-логгирование временно для всего приложения и для отдельных запросов, например, указывая специальный флаг в теле запроса
                                    • Полезные метрики, типа кол-ва запросов в секунду, не считаются через анализ логов, а собираются и аггрегируются самим приложением и пишутся раз в n секунд.
                                    • логи должны содержать requestId, уникальный для каждого запроса, желательно в пределах всей системы, а не только одного приложения.
                                    • для бизнес-аналитики очень полезно иметь структурированные логи, например, в виде json, где назначение каждого поля описано в отдельном документе, и названия полей консистентны для всей системы.

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