31 мар. 2026 г.·7 мин чтения

Версионированные API-клиенты для B2B-приложений на Swift и Kotlin

Версионированные API-клиенты помогают командам на Swift и Kotlin безопасно выкатывать изменения сервера, поддерживать старые версии приложений и не заставлять клиентов обновляться принудительно.

Версионированные API-клиенты для B2B-приложений на Swift и Kotlin

Почему в долгоживущих приложениях это становится проблемой

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

Эта задержка имеет значение. Многие компании выкатывают обновления через IT, процесс согласования изменений, правила управления устройствами или просто по привычке. Если у команды продаж, в клинике или у логистической группы в поле 400 устройств, они не обновят всё во вторник только потому, что ваш бэкенд изменился в понедельник.

Именно здесь начинаются проблемы. Команда сервера добавляет обязательное поле, переименовывает статус, меняет enum или убирает старый endpoint. Новая сборка приложения в тестах работает, но старые сборки всё ещё отправляют старый payload или ждут старый ответ. Пользователь видит не мелкий сбой, а неудачный вход, пропавшие заказы, сломанную синхронизацию или экраны, которые так и не загружаются.

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

Поэтапный выкатыватель намного удобнее. Сервер какое-то время принимает старые и новые запросы, а клиент знает, по какому контракту он работает. Поэтому команды и вкладываются в versioned API clients, а не надеются, что все клиенты всегда будут актуальны.

Цена ошибки быстро растёт. Поддержка часами разбирает тикеты, которые в итоге сводятся к одному изменению API. Инженеры бросают запланированную работу, чтобы срочно выпускать hotfix. Продакт и аккаунт-команды успокаивают раздражённых клиентов. Если проблема задевает биллинг или полевые отчёты, доверие падает очень быстро.

Спешный патч может исправить сегодняшний сбой, но обычно оставляет после себя беспорядок. Гораздо дешевле заранее спланировать работу с разными версиями приложения, ещё до первого breaking change.

Что именно версионировать в клиенте

Ставьте метки версий на границе сети, а не по всему приложению. В Swift и Kotlin это обычно значит endpoint-определения, модели запросов и модели ответов. Экраны, use cases и хранилище должны в основном не замечать, пришли ли данные от сервера как v1 или v2.

Такое разделение важно, потому что транспорт меняется чаще, чем бизнес-правила. CustomerResponseV1 и CustomerResponseV2 могут обе маппиться в один и тот же тип Customer, если смысл в приложении остаётся прежним. Это делает versioned API clients компактнее и снижает количество сбоев из-за серверных изменений.

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

Частая ошибка — версионировать вообще всё, что связано с клиентом. Большинству команд не нужны RetryManagerV2 или LoggerV3. Им нужны новые wire-модели, когда меняется контракт, и общий support-код в остальном.

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

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

  • поле становится обязательным
  • тип поля меняется, например id с числа на строку
  • поле остаётся с тем же именем, но меняет смысл
  • в enum появляется значение, которое старый клиент не умеет обработать
  • успех или ошибка начинают возвращаться в другой форме

Запишите эти правила до начала работы над релизом. Достаточно короткой таблицы в репозитории: «новое nullable-поле = та же модель», «переименованное поле = новая модель», «новое обязательное поле в запросе = новая версия запроса». Одна такая привычка сильно помогает сохранить обратно совместимый API без необходимости заставлять всех клиентов обновляться в один и тот же день.

Простая структура клиента для Swift и Kotlin

Хорошая схема начинается с одного общего сетевого слоя и двух тонких version-модулей над ним. Общий слой один раз делает скучную работу: заголовки авторизации, повторные попытки, подпись запросов, таймауты, логирование и обработку ошибок. Этот код не должен ничего знать ни о /v1, ни о полях ответа /v2.

Выше этого держите каждую версию API в своём модуле. В Swift это может быть отдельный target или группа папок. В Kotlin — отдельный пакет или Gradle-модуль. Идея простая: когда сервер меняется, вы правите version-модуль, а не всё приложение.

Один core, отдельные version-модули

Хорошо работает такая схема:

  • NetworkCore для HTTP-транспорта и общих ошибок
  • ApiV1 для запросов, DTO и парсинга v1
  • ApiV2 для запросов, DTO и парсинга v2
  • Domain для моделей приложения, таких как Customer, Invoice или Order
  • Features для экранов, view models и бизнес-правил

Общий core отправляет запросы. Каждый version-модуль строит свои endpoint и парсит свои ответы. Если v1 возвращает full_name, а v2 — displayName, каждый модуль должен декодировать это поле в своём коде. Не стройте один огромный декодер, полный проверок по версиям.

Преобразуйте всё в одну модель приложения

Экраны не должны знать, какая версия сервера ответила. Им нужна одна стабильная доменная модель.

Например, ApiV1Customer и ApiV2Customer могут обе маппиться в один объект Customer, который использует остальная часть приложения. Как только приложение получает эту доменную модель, логика экрана остаётся прежней. Списку клиентов, поиску и экрану деталей не нужны отдельные ветки для v1 и v2.

Это ещё и упрощает тестирование. Вы можете подменить V1CustomerClient или V2CustomerClient за одним интерфейсом, например CustomerRepository, и прогонять одни и те же тесты экранов для обеих версий.

Одно правило сильно экономит нервы: никогда не делите response-модели между версиями, даже если они почти одинаковые. Общий транспортный код — нормально. Общие app-модели — нормально. Общие wire-модели обычно превращаются в хаос, как только сервер начинает меняться.

Как поэтапно выкатывать изменение на сервере

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

Хороший rollout обычно идёт по одному и тому же сценарию:

  1. Оставьте текущий контракт как v1. Не переименовывайте его и не меняйте форму тихо.
  2. Добавьте модели запросов и ответов v2 рядом с v1 в кодовой базе Swift и Kotlin.
  3. Направляйте трафик на v1 или v2 через проверку возможностей сервера или feature flag.
  4. Выпустите одну сборку приложения, которая умеет читать оба ответа и маппить их в одну и ту же модель приложения.
  5. Убирайте v1 только после того, как использование упадёт, а старый путь останется тихим в течение полного цикла релиза.

Больше всего работы делает именно шаг с маршрутизацией. Если приложение на старте узнаёт, что сервер поддерживает v2, оно может вызвать новый endpoint. Если нет — остаётся на v1. Feature flag тоже подходит, особенно когда нужно включать изменение для одного клиентского аккаунта за раз.

Держите разделение как можно ниже в стеке. Поместите его в сетевой слой или слой репозитория, а не в код экранов. UI по-прежнему должен получать одну чистую доменную модель. Именно так versioned API clients остаются просто удобными в сопровождении, а не превращаются в лабиринт.

Небольшой пример помогает это представить. Допустим, приложение для согласования раньше получало status: "approved", а новый сервер возвращает более богатый объект с status, reason и changedBy. Добавьте v2-модель для новой формы, сохраните v1-модель для старой и преобразуйте обе в один локальный объект ApprovalStatus. QA сможет проверить оба сценария в одной сборке, просто переключив возможность или флаг.

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

Как одновременно работать со старыми и новыми ответами

Исправьте один рискованный endpoint
Начните с endpoint, который чаще всего вызывает обращения в поддержку, и продумайте более безопасный контракт.

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

В Swift новые транспортные поля лучше декодировать как optional, а потом маппить их в app-модели с безопасными значениями по умолчанию. В Kotlin всё работает так же через nullable-поля и значения по умолчанию. Если сервер добавляет priorityLabel, приложение может маппить nil или null в разумный fallback, например normal или пустую метку, в зависимости от экрана.

Такой небольшой выбор предотвращает много поломок. Менеджеру по продажам, который открывает карточку клиента, всё равно, отправил API пять полей или шесть. Ему важно, чтобы экран открылся и данные по-прежнему имели смысл.

Разбирайте каждую форму осознанно

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

Низкоуровневые помощники при этом всё равно можно делить. Разделение должно происходить там, где меняется смысл. Если v1 отправляет customer.name, а v2 — profile.fullName, пусть каждая версия маппит это сама, вместо того чтобы заставлять один парсер гадать.

Здесь помогают несколько рабочих привычек:

  • логируйте, какая версия клиента и какой парсер обработали каждый запрос
  • отслеживайте success- и error-rate отдельно для v1 и v2
  • помечайте аналитические события по версии, чтобы старые и новые потоки не смешивались

Отдельная аналитика экономит время, когда что-то ломается. Если invoice_open_failed растёт только у ответов v1, команда знает, куда смотреть. Если падают обе версии, проблема в другом месте.

Именно здесь versioned API clients и окупаются. Они позволяют серверу меняться, не загоняя всех клиентов в срочное мобильное обновление.

Реалистичный пример выката

Биллинг — это место, где проблема становится очень реальной очень быстро. Один клиент установил ваше приложение три месяца назад и с тех пор не обновлялся. Его бухгалтерия открывает список счетов в старой iPhone-сборке на Swift и старой Android-сборке на Kotlin. Другой клиент обновился сегодня утром и теперь использует новейшую сборку на обеих платформах.

В прошлой версии приложения было только три состояния счетов: open, paid и overdue. Новый релиз сервера добавляет ещё два: partially_paid и pending_review. Он также добавляет короткую заметку, которая подсказывает пользователю следующий шаг, например waiting for finance approval.

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

Более безопасный подход — один backend и две формы ответа. Старое приложение отправляет API version 1. Новое приложение отправляет API version 2. Обе версии обращаются к одному billing service и читают одни и те же записи счетов. Отличается только формат ответа, который бэкенд возвращает в JSON.

Для version 1 сервер сводит новые состояния к значениям, которые старое приложение уже понимает:

  • partially_paid становится open
  • pending_review становится open
  • новое поле с заметкой не отправляется
  • старые поля даты и суммы остаются с теми же именами

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

Поддержке тоже проще разобраться. Если в тикете пишут «статус счета выглядит неправильно», можно посмотреть логи по сборке приложения и версии API. Если ошибки только у версии 1, команда проверяет compatibility mapping. Если проблему сообщают только пользователи version 2, сначала смотрят новый клиентский код.

Вот в чём настоящая ценность. Один клиент может сидеть на релизе прошлого квартала, а другой — на сегодняшней сборке, и оба будут нормально работать с одним и тем же backend.

Ошибки, которые вынуждают клиентов обновляться

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

Большинство принудительных обновлений появляется из-за маленьких серверных изменений, а не из-за больших переписываний. Команда меняет одно поле, убирает одну старую форму ответа или добавляет один тихий fallback. Потом старые версии приложения начинают показывать неверные данные, и поддержка захлёбывается.

Одна из частых ошибок — поменять смысл поля, не меняя версию. Если status раньше означал draft или sent, а теперь означает open или closed, старые клиенты всё ещё смогут это распарсить, но примут неправильное решение. Это хуже, чем явная ошибка, потому что приложение выглядит рабочим, хотя делает не то.

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

Ещё одна ловушка сидит в коде клиента. Когда логика экранов зависит от сырых JSON-имён, любое изменение бэкенда тут же протекает в UI. Более безопасный подход прост: сначала преобразуйте сетевые ответы в app-модели, а уже потом пусть экраны читают эти модели. Версионирование API в Swift и код API-клиента в Kotlin намного проще поддерживать, когда слой представления никогда не заботится о том, отправил сервер account_name или customerName.

Тихие значения по умолчанию наносят другой вред. Допустим, сервер добавляет новое значение enum, а клиент тихо преобразует неизвестные значения в active. Приложение продолжает работать, но теперь оно обманывает пользователя. Хорошие versioned API clients завершаются контролируемо, логируют несоответствие и показывают безопасное fallback-состояние, которое делает проблему заметной.

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

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

Быстрые проверки перед каждым релизом

Проверьте запуск вашего API
Обсудите версионирование, сроки выката и правила отката до следующего релиза.

Релиз может отлично выглядеть в code review и всё равно сломать клиентский аккаунт на следующее утро. С versioned API clients самый безопасный привычный шаг — короткая предрелизная проверка, которая смотрит на реальное поведение, а не только на тесты.

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

Прогоните тот же сценарий в staging и со старым, и с новым контрактом клиента. Используйте реалистичные данные, а не идеальные sample records. Команды часто не замечают ошибки, потому что staging-данные слишком чистые, а в продакшене есть частичные записи, старые состояния и странные комбинации, которые проявляются только в долго живущих аккаунтах.

Перед выпуском проверьте несколько базовых вещей:

  • самая старая поддерживаемая версия приложения всё ещё может выполнить основную задачу без ручного обхода
  • staging-тесты покрывают обе версии с реалистичными аккаунтами и старыми формами данных
  • логи записывают версию клиента, endpoint и понятную причину ошибки
  • у поддержки есть короткая заметка о выкатывании, и она знает, что могут спросить клиенты
  • команда заранее определила дату удаления старого контракта и внесла её в план релиза

Проверка логов важнее, чем кажется многим командам. Когда запрос падает, поддержке не должен быть нужен разработчик, чтобы ответить на базовые вопросы. Если в логах видно v1 /orders failed because status is missing, команда может быстро среагировать. Если в логах только bad request, все теряют время.

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

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

Что делать дальше

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

До следующего спринта запишите короткое правило версионирования, которому команда действительно сможет следовать. Пишите простым языком. Укажите, когда сервер может добавлять поля, когда он обязан сохранять старые поля, когда нужен новый endpoint и как долго поддерживается каждая версия мобильного клиента. На это уйдёт час-два, а позже это может сэкономить дни на очистке.

Затем вместе проверьте зону ответственности. Один мобильный разработчик и один backend-разработчик должны утверждать каждое breaking API change до публикации. Product тоже может участвовать, но у технических владельцев должен быть общий ответ на несколько простых вопросов: какие версии приложения всё ещё активны, что получат старые сборки и как остановить выкатывание, если ошибки начнут расти.

Хорошего первого шага уже достаточно:

  • отметьте нестабильные endpoints в одном общем трекере
  • логируйте версию клиента в каждом API-запросе
  • добавьте тесты и для старой, и для новой формы ответа
  • держите декодирование v1 и v2 раздельным, вместо того чтобы смешивать правила в одну большую модель
  • назначьте реальную дату удаления для каждой старой версии

Если кодовая база уже кажется запутанной, не пытайтесь исправить всё сразу. Начните с одного endpoint, добавьте одну versioned-модель и выпустите одно безопасное изменение на сервере. Этого достаточно, чтобы доказать, что схема работает. После этого versioned API clients перестают выглядеть как лишний процесс и начинают ощущаться как базовая гигиена.

Если вам нужен внешний взгляд, Oleg Sotnikov на oleg.is работает как Fractional CTO и startup advisor и помогает командам с API design, infrastructure и AI-assisted development workflows. Такой второй взгляд особенно полезен, когда нужно поддерживать долго живущие Swift- и Kotlin-приложения, не заставляя всех клиентов обновляться одновременно.

Хороший следующий шаг прост: выберите на этой неделе один рискованный endpoint, определите для него правило совместимости и заставьте следующий релиз сервера ему следовать.

Часто задаваемые вопросы

Почему B2B-приложениям нужно версионирование API?

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

Какие части клиента нужно версионировать?

Версионируйте сетевой слой: endpoints, модели запросов и модели ответов. Интерфейс, хранилище и бизнес-правила лучше оставить на одной стабильной доменной модели, чтобы изменения на сервере не расходились по всему приложению.

Должны ли экраны знать про v1 и v2?

Нет. Пусть UI работает с одной моделью приложения, например Customer или Invoice, а не с сырыми payload из v1 или v2. Преобразуйте каждую версию API в эту модель на уровне репозитория или сетевого слоя.

Когда мне нужна новая модель запроса или ответа?

Создавайте новую модель, когда изменение может сломать поведение. Частые случаи: обязательное поле, новый тип поля, переименованное поле, изменившийся смысл или другая форма success- либо error-ответа.

Может ли одна сборка приложения работать и со старыми, и с новыми ответами API?

Да. Выпускайте одну сборку приложения, которая умеет разбирать обе формы ответа, а затем преобразует их в одну локальную модель. Выбор endpoint может делать проверка возможностей сервера или feature flag.

Что лучше использовать: feature flag или проверку возможностей сервера?

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

Как долго держать старую версию API?

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

Что обычно вынуждает клиентов обновляться?

Обычно команды ломают старые клиенты, когда меняют смысл поля, слишком рано удаляют старые поля или заставляют UI зависеть от сырых имен JSON. Проблемы также возникают, если неизвестные значения enum тихо превращаются в неверное состояние.

Как тестировать изменения API с версиями перед релизом?

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

Нужны ли отдельные сетевые стеки для каждой версии API?

Да. Оставьте один транспортный слой для авторизации, повторных попыток, логирования, таймаутов и ошибок. ApiV1 и ApiV2 держите в отдельных модулях или пакетах поверх этого слоя, а wire-модели не смешивайте, даже если они почти одинаковые.