Skip to content

Latest commit

 

History

History
265 lines (173 loc) · 30.2 KB

13.api_versioning.md

File metadata and controls

265 lines (173 loc) · 30.2 KB

13. Версионирование API

13.1 Введение

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

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

Особенно в мире стартапов, где все менее структурировано. Все начинается с "Opportunities (возможностей)", которые превращаются в "Photo Opps (фото отчет)" и заканчивается все это так называемыми "Campaigns (кампаниями)". Вы можете смеяться, и говорить что это с вами никогда не случится, но всеже это случится. Когда вы будете меньше всего этого ожидать, к вам придут бизнес-требования и как скумбрия мокрым хвостом ударят по лицу. Когда это случится, версионирование API ваше единственное решение.

Конечно, вы можете сказать, что выше API должно поддерживать обратную совместимость - но это не очень реально, когда ваш API должным образом используется на всей линейке продуктов. Чтобы продемонстрировать дальше, предположим, что у вас есть 30 приложений (и возможно горстка внешних компаний, использующих API), каждое из которых опирается на "клиентский" REST ресурс - тогда вы можете выбрать следующее:

  1. Сохранять обратную совместимость (и потерять продаж на 1млн. долларов, потомучто вы не можете реализовать крутую функцию "X")
  2. Внести изменения во все 30 приложений одновременно для обработки новых данных (но вам, вероятно, не хватит ресурсов чтобы сделать это вовремя и в срок)
  3. Сделайте изменения, нарушив работу приложений, но получите продажи (конечно вы можете исправить оставшиеся приложения в будущем, правильно?) Источник: thereisnorightway.blogspot.com.tr

13.2 Различные подходы в версионировании API

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

На протяжении всей главы будут отсылки на различные популярные сервисы с публичным API и типами используемой версионности. Спасибо Тиму (Tim Wood) за составление обширного списка “How are REST APIs versioned?”, на который буду часто ссылаться в этой главе.

Подход №1: URL

Указывание номера версии в URL является очень распространенной практикой среди популярных публичных API.

По сути, вы просто добавляете v1 или 1 в URL, потому указать следующую версию не составит труда.

https://api.example.com/v1/places

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

У Twitter имеется две версии: /1/ и /1.1/, которые являлись рабочими на момент написания главы. Это дает разработчикам возможность обновить код, который ссылается на старые конечные точки (endpoints), так что они могут использовать новые. Большенство API назвали бы это как версию 2, но там не было достаточно значительных изменений, возможно по этому они выбрали менее значимый номер.

Некоторые говорят, что URL версионность позволяет более удобно "копипастить" URL, чем другие подходы (многие из которых включают HTTP заголовки), и что их мол проще поддерживать.

Это можеть быть верным в некоторых случаях, но вообще, слегка неправильно. Не RESTful API никогда не будут полностью удобны для "копипаста", потому что там всегда будут использоваться заголовки: Cache-Control, Accept, Content-Type, Authorization, и т.д. Попытка полностью уместить API запрос в URL выглядит очень глупо.

Аргумент "копипаста" является незначительным плюсом, по сравнению с тем какие у этого подхода есть потенциальные недостатки.

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

Например, если вы храните URL конечной точки (endpoint) в вашей базе данных для дальнейшего использования, то выглядеть это может примерно так:

https://api.example.com/v1/places/213

Однажды вы получаете email от example.com, в котором говорится, что их API v1 через три месяца больше не будет поддерживаться, и вам нужно по возможности начать пользоваться версией API v2.

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

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

https://api.example.com/v2/places/213

Это могло бы сработать, но факт в том, что в письме было замечание, в котором говорится что авто-инкрементный ID более не используется в их URL (они где-то прочитали, что это плохое решение) и решили использовать вместо него строковый идентификатор (slug):

https://api.example.com/v2/places/taksim-bunk-hostel

Что теперь? Единственное решение в данном случае, это создать скрипт, который пройдется по каждой записи в вашей базе данных, дернет для этой записи ихний API v1 и получит необходимую информацию (в надежде что этот строковый идентификатор slug доступен) и затем сформирует URL совместимый с API v2 для сохранения.

Если вы это сделаете с несколькими миллионами записей, тогда вероятно быстро достигнете какого-нибудь лимита на запросы к API. Twitter, например, в некоторых случаях лимитирует доступ приложения к конечной точке (endpoint) на 15 минут за 15 запросов, так что в таком случае потребуется около двух недель для обновления 1 миллиона записей.

Может это выглядит как крайний случай, но размещение версии API в URL порождает ряд всевозможных непонятных проблем, и вынуждает ваших разработчиков вручную формировать URL ресурса с заменой строки, что выглядит довольно грубо. Питер Уильямс (Peter Williams) указал на это в статье под названием “Versioning REST Web Services” еще в 2008 году, но кажется, все его последовательно игнорируют.

Еще одним недостатком этого подхода является то, что указывать v1 и v2 к разным серверам может быть трудно, если вы не используете какой-ниубдь Apache Proxy или Nginx как прокси. В общем то, многие системы ожидается размещать на том же сервере иначе могут появиться накладные расходы. Так например, если v1 работает на PHP а v2 на Scala, вы можете столкнуться с некоторыми проблемами если все это запускать на одном и том же сервере.

Обратная сторона проблемы "трудоемкого размещения всего на одном сервере", это когда API разработчики используют единую кодовую базу обеспечивая версионность внутри самого web-приложения. Они просто создают роуты (routes) с префиксом /v1/places, затем, когда им нужно сделать версию v2, они копируют роуты, копируют контроллеры и кое-что донастраивают. Это может быть сделано, если вы также версионируете свои трансформеры (transformers - для поддержания структуры данных и типов данных), и уверены в том, что весь общий код (библиотеки, пакеты и т.д) будет поддерживаться и оставаться совместимым на всем протяжении. Это редкий случай, и люди добавляют v1 в их URL адреса просто потому, что это единственное решение, которое они знают.

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

Если API версии очень похожи (тотже язык, тотже фреймворк и т.д.), тогда вы можете просто делить историю в GIT - будь то другая ветка в том же API репозитории или просто другая ветка. Некоторые люди используют модель Git Flow и добавляют номера версий, так один репозиторий может иметь следующие ветки:

  • 1.0/master
  • 1.0/develop
  • 2.0/master
  • 2.0/develop

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

Популярные API

  • Bitly
  • Disqus
  • Dropbox
  • Bing (lol)
  • Etsy
  • Foursquare
  • Tumblr
  • Twitter
  • Yammer
  • YouTube

Плюсы

  • Невероятно прост для API разработчиков
  • Невероятно прост для API пользователей
  • Удобные для URLы для копипаста

Минусы

  • Технически не является RESTful
  • Сложен при размещении на отдельные сервера
  • Принуждает пользователей API прибегать к сложным и непонятным действиям для поддержания ссылок (links) в актуальном состоянии.

Подход №2: Имя хоста

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

https://api-v1.example.com/places

На самом деле, это не решает некоторые другие проблемы. Имея версию в URL в целом (URI или поддомене), перенимаются все те же проблемы для пользователей API, но по крайней мере уменьшаются шансы, что разработчики API будут использовать единую кодовую базу.

Плюсы

  • Невероятно прост для API разработчиков
  • Невероятно прост для API пользователей
  • Удобные для URLы для копипаста
  • Простое использование DNS для распределения версий на несколько серверов.

Минусы

  • Технически не является RESTful
  • Принуждает пользователей API прибегать к сложным и непонятным действиям для поддержания ссылок (links) в актуальном состоянии.

Подход №3.1: Тело и параметры запроса

Если вы решили уйти от размещения версии в URI, тогда одно из двух других мест, где её можно указать, это само тело HTTP запроса.

POST /places HTTP/1.1
Host: api.example.com
Content-Type: application/json
{
    "version": "1.0"
}

Это решает проблему с изменением URL в будущем, но может привести к противоречивому опыту. Если разработчик API постит JSON или похожую структуру данных, тогда это просто, но если ему нужно установить Content-Type как image/png или даже text/csv, то довольно быстро возникают сложности.

Некоторые предполагают, что решение этой проблемы в том, чтобы перемести параметр версии в строку запроса, но тогда версия снова попадает в URL! И тогда сразу многие проблемы первых двух подходов возвращаются.

POST /places?version=1.0 HTTP/1.1
Host: api.example.com
 
header1,header2
value1,value2

Значит... просто надо сделать что-то другое. Многие PHP фреймворки игнорируют строку запроса отправленную любым методом кроме GET, что идет вразрез с HTTP спецификацией но остается широко распространенным явлением. Имея такой параметр, который вращается внутри различных типов контента в теле запроса или иногда в URL или даже всегда в URL, независимо от HTTP метода, при использовании может ввести в заблуждение.

Популярные API

  • Netflix
  • Google Data
  • PayPal
  • Amazon SQS

Плюсы

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

Минусы

  • Различные виды Content-type требуют различные параметры и для некоторых (например CSV) такой вариант просто не подходит.
  • Принуждает пользователей API прибегать к сложным и непонятным действиям для поддержания ссылок (links) в актуальном состоянии, если параметр версии находится в строке запроса

Подход №3.2: Кастомный заголовок запроса

Итак, если URL или тело HTTP запроса не очень подходящее место для размещения информации о версии API, то что остается? Конечно, это заголовки!

GET /places HTTP/1.1
Host: api.example.com
BadApiVersion: 1.0

Этот пример был приведен Марком Ноттингемом, который на момент написания этой главы является председателем рабочей группы IEFT HTTPbis Working Group. Эта группа отвечает за пересмотр протокола HTTP 1.1 и работает над протоколом HTTP 2.0. Вот что он говорит о кастомном заголовке с версией:

Это плохо и не правильно по многом причинам. Почему?

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

HTTP/1.1 200 OK  
BadAPIVersion: 1.1  
Vary: BadAPIVersion  

С другой стороны, промежуточные кеши могут отдать клиентам неверный ответ. (например, ответ 1.2 на клиент 1.1 или наоборот)

Источник: Mark Nottingham, “Bad HTTP API Smells: Version Headers”

Без указания заголовка Vary, возникают трудности с системами кеширования, например Varnish не сможет понять, что кто-то запрашивает версию 1.0, потому что URL не сильно отличается от запроса версий 1.1 или 2.0. Это может стать большой проблемой, поскольку пользователи API запрашивают конкретную версию, а не какую-то другую.

Такой сложный вопрос кеширования просто очень напрягает. Если вы используете кастомный заголовок, тогда пользователи API должны пойти и посмотреть в вашей документации упоминание об этом и запомнить как его использовать. Может это будет API-Version или Foursquare-Version или X-Api-Version или Dave. Кто-то знает, а кому-то нужно помнить.

Популярные API

  • Azure

Плюсы

  • Прост для пользователей API (если они знают про заголовки)
  • URL остается таким же
  • Технически это больше похоже на RESTful, чем указывание версии в URI

Минусы

  • Системы кеширования могут запутаться
  • Разработчики API могут запутаться (если они не знают про заголовки)

Подход №4: Распределение контента

Заголовок Accept спроектирован для того, чтобы попросить сервер выдать ответ для определенного ресурса в определенном формате. Традиционно, многие разработчики задумываются о нем только в случае (X)HTML, JSON, изображения и т.д., но область применения такого заголовка более обширна. Если мы можем спокойно попросить выдать нам данные с контентом в соответствующем формате, имеющий соответствующий синтаксис, тогда почему бы не воспользоваться этим же заголовком и для указания версии тоже?

GitHub до сих пор следует советам многих людей, упомянутых в этой главе, и используют заголовок Accept для отдачи различных типов данных.

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

application/vnd.github[.version].param[+json]

Самые базовые типы, которые поддерживает API:

application/json
application/vnd.github+json

Источник: GitHub, “Media Types”

Обычно, если вы запрашиваете application/json или application/vnd.github+json, то получите в ответ JSON. Без указания в дальнейшем, они будут вам показывать текущий ответ по умолчанию, который на момент написания является v3, но через некоторое время может и поменяться на v4. Они предупреждают, что если вы не будете указывать версию, тогда ваше приложение может сломаться! Логично.

Для указания версии вы должны использовать заголовок Accept: application/vnd.github.v3+json, тогда при изменении версии по умолчанию когда-нибудь в будущем на v4, ваше приложение продолжит работать с версией v3.

Такой подход решает проблему кеширования, решает проблемы манипуляций с URL как у подходов с указанием версионности в URL, и его можно считать более RESTful, но для некоторых разработчиков может показаться довольно запутанным. Может их надо обучить, натренировать, чтобы привыкли к новому подходу, или запретить версионность на основе URL, в любом случае такой подход семантически более правильный и в целом работает очень хорошо. Такой подход используют в Kapture для внутреннего API, и это работает без проблем.

Единственным недостатком является тот, который присутствует у всех ранее упомянутых подходов: Если версия охватывает API целиком, то это создаст очень большие трудности его разработчикам, при обновлении функционала ихнего приложения. Даже если только 10% от функционала API изменится между версиями, то изменение версии всего API в целом может напугать разработчиков. Даже со списком изменений, разработчику будет трудно узнать, заработает ли его приложение полностью после переключения на другую версию API. Даже обширный набор тестов не в состоянии уловить проблему на стороне стороннего сервиса, потому что многие разработчики используют жестко прописанные JSON ответы в своих unit-тестах.

Если изменение версии всего API будет слишком значительно, то как вариант можно сменить версию лишь части API.

Популярные API

  • GitHub

Плюсы

  • Прост для пользователей API (если они знают про заголовки)
  • URL остается таким же
  • HATEOAS-совместимый
  • Кеш-совместимый
  • Sturgeon-approved (нуу.. наверно "осетр одобряе")

Минусы

  • Разработчики API могут запутаться (если они не знают про заголовки)
  • Версионирование целиком всего API может запутать пользователей (но и все другие походы имеют туже проблему)