18 июн. 2025 г.·8 мин чтения

Генерированные API-клиенты без лишнего шума в репозиториях для web и mobile

Генерированные API-клиенты могут экономить время, но неудачное размещение создаёт шумные diff'ы и замедляет релизы. Сравните процесс ревью и сроки для web- и mobile-команд.

Генерированные API-клиенты без лишнего шума в репозиториях для web и mobile

Что идёт не так, когда codegen оказывается не в том репозитории

Небольшое изменение в API может превратиться в шумный pull request, если сгенерированные API-клиенты лежат не там, где нужно. Одно новое поле в ответе должно быть простым для проверки. Вместо этого команда получает правку спецификации плюс 2 000 изменённых строк сгенерированного кода, обновления lockfile и файлы, которые никто не трогал вручную.

Этот шум очень быстро ухудшает качество ревью. Backend-инженер знает, что эндпоинт изменился, но web- или mobile-ревьюеру теперь приходится пролистывать страницы сгенерированного вывода, чтобы понять, не проскочило ли что-то странное. Большинство людей перестают читать внимательно. Они просто одобряют diff, потому что по-настоящему проверить его уже сложно.

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

У web-команд своя проблема. Они тянут свежий сгенерированный код даже тогда, когда меняли только текст на странице или состояние кнопки. Их ветка теперь содержит обновления API-клиента, которых они не просили. В файловых конфликтах появляются непонятные им файлы, и простая фронтенд-задача начинает ощущаться тяжелее, чем должна.

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

Типичный пример — API для заказов, где добавили одно необязательное поле. Реальное изменение крошечное. Но при неправильной организации репозитория оно превращается в шум на ревью, задержки для mobile, лишние pulls во фронтенде и тихий дрейф спецификации. После нескольких таких циклов команды перестают доверять codegen и начинают обходить его вручную.

Где можно разместить сгенерированных клиентов

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

Один из распространённых вариантов — backend-репозиторий. Так спецификация и сгенерированный код находятся рядом, а серверные изменения и обновления клиента попадают в один коммит. Это хорошо работает, когда обе стороны контролирует одна команда, а web-разработчики часто подтягивают обновления. Минус становится очевиден уже через несколько недель: backend pull request'ы заполняются машинно сгенерированными файлами, которые большинство ревьюеров даже не читает, а mobile-командам всё равно нужен удобный способ использовать результат.

Другой вариант — генерировать клиента прямо внутри каждого web- или mobile-репозитория. App-командам это нравится, потому что они сами решают, когда обновляться, тестировать и выпускать новую версию. Для mobile это особенно удобно, поскольку iOS и Android часто закрепляют версию клиента для ветки релиза. Но цена — дублирование настройки. Если три приложения генерируют один и тот же клиент в трёх репозиториях, у вас теперь три места, где codegen может сломаться.

Отдельный репозиторий или упакованный результат

Отдельный package-репозиторий даёт сгенерированному коду свой дом. Обычно это делает ревью чище. Backend-разработчики меняют спецификацию, автоматизация публикует новую версию клиента, а app-команды обновляются тогда, когда готовы. Такая схема часто легче в сопровождении, когда потребителей уже больше одного. Но она добавляет ещё один процесс релиза, и кому-то нужно следить за версиями пакета.

Самая чистая схема, если хочется убрать repo churn, — вообще не коммитить сгенерированный source. CI может собрать клиент из protobuf- или REST-описаний и выдать артефакт вместо файлов в репозитории. Web-приложения просто ставят пакет как обычную зависимость. Mobile-команды могут зафиксировать версию и работать в своём темпе. Вы меняете видимые diff'ы на более сильную автоматизацию, теги релизов и воспроизводимые сборки.

Небольшой пример хорошо показывает разницу. Допустим, API добавляет одно необязательное поле во вторник. Web-команде это поле может понадобиться в тот же день. Mobile-команда, возможно, не будет выпускаться ещё две недели. Если сгенерированный код лежит только в app-репозиториях, обеим командам придётся отдельно запускать генерацию и коммитить изменения. Если CI публикует версионированные артефакты, web может сразу перейти на новый клиент, а mobile останется на старом до следующего релиза приложения.

Для одного backend'а и одного быстро меняющегося web-приложения backend-репозитория часто достаточно. Для нескольких потребителей, особенно для mobile, package-репозиторий или артефакты, собранные в CI, обычно создают меньше шума и меньше споров на ревью.

Как protobuf и REST-спецификации меняют выбор

Формат спецификации меняет не только сгенерированные файлы. Он меняет то, насколько предсказуемы ваши diff'ы, сколько настройки нужно и смогут ли web- и mobile-команды использовать один и тот же формат клиента.

С protobuf генерация обычно строже. Типы задаются явно, номера полей фиксируют совместимость на уровне wire-формата, а результат при изменении схемы обычно меняется меньшими и более аккуратными diff'ами. Если зафиксировать версию генератора, командам проще проверять такие обновления, не гадая, изменилась ли шаблонная логика или правило именования.

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

С OpenAPI всё иначе. Спецификация может хорошо описывать API, но результат генерации сильно зависит от настроек генератора, шаблонов, правил nullable, обработки enum'ов и опций именования. Две команды могут смотреть на одну и ту же спецификацию и всё равно получить разные клиенты, если эти настройки не зафиксированы. Поэтому многие команды предпочитают держать конфигурацию и шаг генерации ближе к приложению или поддерживать отдельный SDK-репозиторий с очень строгими инструментами.

Mobile-поддержка тоже важна. gRPC хорошо работает на Android и iOS, но браузеру часто нужен другой транспорт, например gRPC-Web, или отдельный слой-прокси. Это может изменить формат SDK, который вы публикуете. Mobile-команде могут понадобиться сгенерированные protobuf-stubs плюс тонкая обвязка, а web-команде — собственный клиент, который скрывает особенности транспорта.

REST обычно лучше ложится в browser- и app-стек. REST-клиент может работать поверх fetch в web, URLSession в iOS и OkHttp в Android, не таща за собой много runtime-механики. Это часто означает меньше сюрпризов, когда команды обновляют клиент.

Простой способ выбрать:

  • Используйте protobuf, когда важнее всего жёсткость контракта и нужны более маленькие, более проверяемые diff'ы.
  • Используйте генерацию OpenAPI, когда ваш API изначально REST-first и команды уже живут в стандартных HTTP-стэках.
  • Публикуйте отдельные формы SDK, если mobile может использовать gRPC напрямую, а web — нет.
  • Жёстко фиксируйте версии генератора и конфигурацию, особенно для генерации REST-клиентов.

Один распространённый split работает хорошо: protobuf для внутренних контрактов между сервисами, REST-клиенты для приложений, которые смотрят наружу, в браузер. Это не единственная схема, но она убирает много лишнего шума.

Выберите схему пошагово

Начните с простого инвентаря. Выпишите все команды и приложения, которые используют API, даже те, о которых забывают на планировании. Обычно это web-приложение, iOS-приложение, Android-приложение и любой внешний инструмент для партнёров или внутренний скрипт.

Потом разделите этих потребителей на две группы: тем, кому сгенерированный source нужен в их собственном коде, и тем, кому достаточно опубликованного пакета. Этот один выбор уже убирает много repo churn. Web-приложение, которое собирается много раз в день, может спокойно подтягивать обновление пакета, а mobile-приложению могут понадобиться закреплённые версии, чтобы QA тестировала по одному релизу клиента за раз.

  1. Сопоставьте всех потребителей и способ их выпуска. Браузерное приложение обычно может принять изменения быстрее, чем релиз через App Store. Партнёрскому инструменту может понадобиться более медленный и стабильный путь обновления.
  2. Выберите формат доставки для каждого потребителя. Если команде нужно отлаживать сгенерированный код или патчить шаблоны, держите source ближе к ней. Если она просто вызывает API, публикуйте пакет и не храните сгенерированные файлы в app-репозитории.
  3. Задайте ритм обновлений до того, как писать автоматизацию. Одни команды могут принимать обновления клиента при каждом merge. Другим удобнее один раз в день публиковать новую версию. Mobile-команды часто предпочитают обновляться только на cut релиза, потому что смена клиента посреди цикла создаёт больше тестовой работы.
  4. Назначьте одного владельца для конфигурации генератора, правил версий и политики поломок. Без одного владельца маленькие правки шаблонов быстро превращаются в споры между четырьмя репозиториями.

Небольшой пример хорошо показывает компромисс. Если web-команда деплоится десять раз в день, она обычно может достаточно точно следить за версиями пакета. Если iOS и Android выпускаются реже, им лучше забирать помеченную версию клиента по графику, который они могут протестировать.

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

Настройте flow ревью, которым люди действительно будут пользоваться

Усилите контроль над спецификациями
Приведите в порядок protobuf или OpenAPI-процесс, пока дрейф и ручные правки не расползлись дальше.

Большая часть боли на ревью начинается тогда, когда люди сначала читают 2 000 строк сгенерированного кода, а уже потом доходят до одного поля, из-за которого всё и изменилось. Это неправильный порядок. Для сгенерированных API-клиентов ревьюерам нужно сначала смотреть на изменение контракта, а не на output.

Хороший pull request делает человеческое решение легко заметным. Если кто-то добавляет новое значение enum, меняет имя поля или делает поле необязательным, этот diff должен быть вверху ревью. Сгенерированные файлы важны, но в основном как доказательство того, что генератор всё ещё соответствует спецификации.

Для web- и mobile-команд хорошо работает простой flow:

  • Сначала смотрите на diff в protobuf или REST-спецификации.
  • Изменения конфигурации генератора держите в том же pull request.
  • Скрывайте или сворачивайте сгенерированные файлы, когда проверяете логику приложения.
  • Пусть CI заново генерирует клиентов и падает, если результат не совпадает.

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

Полезно и немного разделить роли на ревью. Один человек проверяет, соответствует ли API-изменение продукту и поведению приложения. Другой смотрит, по-прежнему ли настройка генератора выдаёт аккуратных клиентов. Большинству коллег не нужно читать каждую машинную строку, если diff не выглядит странным.

Небольшой пример показывает, почему это работает. Допустим, backend-разработчик добавляет новое поле "locale". Web- и mobile-ревьюеры сначала решают, подходит ли имя поля, тип и поведение по умолчанию для приложения. После этого они могут пробежать глазами сгенерированный код в поисках сюрпризов, а не изучать каждый файл построчно.

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

Подстройте генерацию под сроки релизов web и mobile

Сгенерированные API-клиенты должны следовать ритму релизов каждой команды, а не одному общему расписанию. Web-команды обычно могут часто забирать свежий клиент, быстро его тестировать и выпускать в тот же день. Mobile-команды работают по более длинному циклу, потому что review в App Store, release train'ы и ограничения на hotfix'ы неизбежно добавляют задержку.

Относитесь к web и mobile по-разному

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

Mobile нужна большая стабильность. Если iOS и Android уже близки к release freeze, даже безопасное изменение в сгенерированном коде может создать шум в QA, snapshot-тестах и на последней стадии approvals. Поэтому многие команды группируют низкорисковые изменения API до mobile-freeze вместо того, чтобы отправлять каждое небольшое обновление в приложение сразу.

Простой ритм работает хорошо:

  • Web забирает сгенерированные обновления коротким циклом, часто при каждом изменении спецификации или по ежедневному расписанию.
  • Mobile обновляется в заранее запланированные контрольные точки, обычно привязанные к следующему релизу приложения.
  • Команды группируют небольшие добавочные изменения API до mobile-freeze.
  • Сервер продолжает поддерживать старые mobile-клиенты, пока достаточное число пользователей не перейдёт на новую версию приложения.

Выпускайте версию клиента, когда важны сроки

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

Это даёт web и mobile понятную цель. Web может перейти на неё быстро, а mobile может зафиксировать эту версию, закончить QA и выпустить всё по своему графику.

Небольшой пример делает это особенно наглядным. Допустим, сервер добавляет новое необязательное поле для web-checkout и одновременно меняет правило валидации, которое использует mobile. Web может сразу взять свежий клиент. Mobile лучше подождать помеченную версию клиента, протестировать новое поведение сервера и выпускаться только когда окно App Store безопасно.

Если сроки релизов разные, workflow клиента тоже должен быть разным. Один график для всех звучит аккуратно, но обычно он создаёт repo churn из-за codegen и торопливые mobile-обновления.

Одно API-изменение для web и mobile

Снизьте трение между командами
Помогите frontend, backend и mobile-командам двигаться в своём темпе, а не по одному графику обновлений.

Небольшое изменение в API может очень быстро превратиться в хаос. Допустим, endpoint оформления заказа теперь требует ещё одно поле — например, tax ID или заметку к доставке, — и сервер будет отклонять запросы без него.

Web-команда обычно может перейти в тот же день. Они обновляют форму, подтягивают новую версию package с клиентом, тестируют новый запрос и выкатываются до конца дня.

Mobile-команды живут по другому времени. iOS и Android могут потребовать UI-работу, QA, проверку в магазине приложений и слот релиза, который откроется только на следующей неделе. Это нормально. Проблема начинается, когда сгенерированный код лежит внутри каждого app-репозитория, потому что одно API-изменение внезапно задевает три кодовые базы, прежде чем все три команды будут готовы.

Опубликованный пакет держит шум под контролем. Схема меняется в API- или client-package-репозитории, там же запускается генерация, и команда публикует новую версию. Web-приложение может перейти на неё сразу. Mobile-приложения могут пока не трогать её до своего окна релиза.

Получается гораздо более чистая последовательность:

  • API-команда добавляет поле и публикует новую версию клиента.
  • Web-команда сразу обновляется и меняет форму оформления заказа.
  • iOS и Android пока остаются на текущей версии пакета.
  • Mobile-команды обновляются, когда готов их следующий релиз.

Такая схема лучше всего работает, если сервер даёт старым версиям приложения короткий grace period. Если новое поле действительно нужно всем прямо сейчас, старые mobile-сборки всё равно сломаются, как бы аккуратно ни был устроен workflow сгенерированных API-клиентов. На практике команды часто принимают старый запрос ограниченное время, подставляют значение по умолчанию на сервере или вводят новую версию endpoint'а, пока mobile не догонит.

Package-репозиторий сам по себе не решает сроки релизов. Он решает repo churn из-за codegen. И это уже большое преимущество, потому что каждая команда обновляется тогда, когда у неё есть причина, а не потому что где-то в другом месте поменялись сгенерированные файлы.

Ошибки, которые создают repo churn

Repo churn обычно начинается с небольшой привычки, а не с большой ошибки в архитектуре. Команды относятся к сгенерированным API-клиентам как к обычным исходникам, а потом маленькая правка API превращается в сотни изменённых строк. Ревьюеры перестают доверять diff'у, и люди вмерживают код, который по-настоящему даже не читали.

Одна из частых проблем — drift генератора. Если один разработчик запустил protobuf- или REST-генератор одной версии, а другой — более новой, одна и та же спецификация может породить разные импорты, именование, порядок файлов или комментарии. В самом API ничего важного не изменилось, но Git всё равно наполняется шумом. Зафиксируйте версию генератора в CI или запускайте генерацию в одной контролируемой среде.

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

Команды также создают шум, когда заново генерируют каждый клиент из-за не связанного с ним изменения endpoint'а. Небольшая правка одного сервиса не должна заставлять заново пересобирать вывод для web, iOS и Android, если нужен только один клиент. Такая схема создаёт огромные diff'ы, лишнее время на ревью и бессмысленные релизы. Mobile-команды страдают от этого сильнее, чем web, потому что их обновления идут медленнее и требуют больше координации.

Последняя ошибка — не писать заметки о версии и заставлять всех сравнивать сырой код. Сгенерированные файлы редко хорошо объясняют намерение. Короткая заметка вроде "добавлено одно поле в OrderSummary" или "status_code переименован в statusCode в TypeScript-клиенте" экономит реальное время на ревью. Oleg часто подталкивает команды к такой дисциплине в AI-first delivery work, потому что чистые change notes становятся ещё важнее, когда автоматизация быстро создаёт большие diff'ы.

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

Быстрые проверки перед тем, как зафиксировать процесс

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

Схема генерации клиента кажется нормальной, пока первая спешная правка API не прилетает в пятницу днём. Тогда слабые места проявляются быстро: шумные diff'ы, сломанные сборки и одна команда, заблокированная календарём релизов другой команды. Короткая предварительная проверка экономит много уборки потом.

Начните с теста на ревью. Если ревьюер не может найти реальное изменение API примерно за две минуты, схема слишком шумная. Обычно это значит, что сгенерированные файлы скрывают важный diff или что изменение спецификации и сгенерированный результат попадают в один гигантский pull request без структуры. Сделайте diff по спецификации легко читаемым, а вывод генерации — достаточно предсказуемым, чтобы его можно было просмотреть, а не выискивать нужное в тысячах строк.

Потом проверьте независимость релизов. Web и mobile почти никогда не выпускаются по одному графику, и если делать вид, что это не так, появится лишнее трение. Web-команде может понадобиться новый клиент уже сегодня, а mobile — дождаться сроков App Store, QA или обратной совместимости. Если один общий процесс заставляет обе команды двигаться вместе, ждите задержек и давления на откат изменений.

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

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

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

Если ответы живут только в Slack или в голове одного старшего инженера, процесс неизбежно начнёт плыть.

Небольшой пример это хорошо показывает. Допустим, backend добавляет новое необязательное поле в ответе заказа. Web-команда может перейти на свежий клиент в тот же день. Mobile-команда может подождать до следующего окна релиза. Это нормально, если спецификация, генерация кода и релизные шаги достаточно разделены, чтобы каждая команда могла двигаться в своём темпе.

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

Что делать дальше для более чистого workflow клиентов

Не переделывайте всё сразу. Выберите один сервис, одну модель размещения и один месяц, чтобы проверить это на реальной web-команде и реальной mobile-команде.

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

В течение месяца отслеживайте несколько простых метрик:

  • сколько сгенерированных файлов затрагивает каждое изменение API
  • сколько времени занимают ревью, когда codegen входит в изменение
  • сколько дней проходит между изменением спецификации и релизом в web и mobile
  • как часто кому-то приходится вручную чинить генерацию

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

Прежде чем на клиент начнут опираться другие команды, запишите правила версионирования на одной короткой странице. Решите, когда изменение считается patch, minor или major. Решите, кто публикует новую версию клиента, и решите, может ли mobile безопасно пропускать версии, когда мешают сроки App Store.

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

Простой пилот часто работает так: web забирает самый новый клиент при каждом merge, а mobile обновляется по фиксированному графику, например раз в спринт. Такая схема очень быстро показывает, подходят ли ваши сгенерированные API-клиенты для обоих темпов релиза или одной команде нужен другой путь.

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

Некоторым командам хочется ещё одни глаза, прежде чем зафиксировать такое решение. Oleg Sotnikov может посмотреть на API-спеки, CI и сроки релизов в роли fractional CTO. Такой внешний обзор особенно полезен, когда команда уже выпускает продукт, но мелкие процессные проблемы всё время возвращаются в виде repo churn.