Рефакторинг Go-API с Claude Code: шаблон промпта, приемы сохранения публичных контрактов и набор автоматических проверок перед мерджем.

«Сломать контракт» API значит изменить внешнее поведение так, что клиентский код перестает работать или начинает вести себя иначе. Контракт - это не только путь и метод. Это весь набор ожиданий: какие поля можно отправлять, что именно приходит в ответе, какие коды ошибок возвращаются и какие заголовки выставляются.
Чаще всего клиенты ломаются не потому, что вы поменяли бизнес-логику, а из-за мелочей на границе системы. Вы «наводите порядок» в структурах и переименовываете user_id в userId, убираете пустые поля из JSON или начинаете возвращать 404 вместо 200 с ошибкой в теле. Для сервера это выглядит разумно, а для клиента - внезапный разрыв договоренностей.
Обычно в контракт входят:
400, где 401, что означает 409.Content-Type, кеширование, id запроса, CORS.Почему это часто случается при рефакторинге с помощником (например, при рефакторинге Go-API с Claude Code)? Модель хорошо улучшает структуру кода, но так же легко «улучшает» и внешние ответы: меняет тексты ошибок, нормализует JSON, предлагает «более правильные» коды. Если заранее не зафиксировать, что именно считается публичным поведением, такие правки выглядят допустимыми.
Относительно безопасны изменения, которые не выходят за пределы внутренних пакетов и не меняют вход-выход хендлеров: переименование внутренних функций, разбиение файлов, перенос логики в сервисный слой, добавление логирования (если оно не утягивает лишние данные наружу).
Почти всегда рискованны изменения на границе API: структуры DTO, теги json, обработка null и пустых значений, семантика ошибок, правила валидации, дефолты, middleware (CORS, авторизация), сериализация времени и чисел.
Правило простое: внутренности можно менять смело, публичное поведение нужно держать «прибитым». Для этого сначала фиксируем контракт, а потом строим проверки, которые поймают любую случайную «косметику» еще до merge.
Перед рефакторингом зацементируйте то, что видит внешний клиент. Контракт - это не только список эндпоинтов, но и то, как сервис ведет себя «на краях»: коды статуса, формат ошибок, значения по умолчанию, а также разница между null и отсутствием поля.
Начните с инвентаризации публичных точек. Описывать нужно не «как устроен код», а что доступно снаружи: роуты, методы, обязательные заголовки, query-параметры, структуры JSON и типовые ошибки. Если API используется мобильным приложением или фронтом, считайте эти клиенты частью контракта: их ожидания часто строже, чем кажется.
Чтобы зафиксировать контракт быстро (даже без формальной спецификации), соберите минимум артефактов, которые можно сравнить до и после:
Content-Type, Authorization, ETag, Cache-Control (то, что реально используют клиенты и инфраструктура).Неявные контракты ломаются особенно незаметно. Типичные случаи: поменяли тип поля (int -> string), начали возвращать пустую строку вместо отсутствия поля, изменили формат даты, поменяли приоритеты валидации и теперь на тот же запрос приходит другая ошибка.
Граница между рефакторингом и новой версией API проста: если клиенту нужно что-то менять, это уже не «чистый» рефакторинг. Признаки, что вы задели контракт:
Пример: есть GET /users/{id}, и клиент ожидает, что при отсутствии пользователя будет 404 с JSON {"error":"not_found","message":"user not found"}. Даже если новый код «логичнее» возвращает 200 и пустой объект, это уже смена контракта. Сначала фиксируйте текущий ответ как эталон, и только потом меняйте внутренности.
Перед тем как трогать код, полезно зафиксировать поведение API как факт. Тогда рефакторинг превращается из угадайки в проверяемую задачу: поменяли реализацию, снаружи осталось как было.
Соберите минимальный smoke-набор: 10-20 запросов, которые чаще всего делают клиенты, или которые критичны для бизнеса. Включите и успехи, и типовые ошибки (невалидный JSON, отсутствующий параметр, несуществующий ресурс).
Для каждого запроса заранее решите, что именно считается эталоном. Обычно достаточно:
Content-Type, Cache-Control, Location, Request-Id);Дальше «заморозьте» окружение, иначе вы не поймете, что вызвало расхождения: код или условия. Зафиксируйте конфиги, фичефлаги, версии зависимостей и тестовые данные. Если поведение зависит от времени, используйте фиксированное время через конфиг или тестовую обертку.
Отдельно полезно заранее записать, что точно нельзя менять без отдельного согласования: формат ошибок, статусы, правила заголовков, поведение по null и пустым значениям, идемпотентность. Это экономит часы споров после того, как помощник «слегка улучшил» ответы.
Когда вы просите ИИ делать рефакторинг, главная опасность не в ошибках компиляции, а в тихих изменениях поведения: другой статус-код, другое поле в JSON, другая обработка пустых значений. Поэтому промпт должен сначала зафиксировать, что считается внешним контрактом, и только потом разрешать внутренние перестановки.
К внешним контрактам обычно относятся HTTP-часть (пути, методы, коды, заголовки), JSON-схемы (поля, типы, обязательность, значения по умолчанию), публичные Go-символы (exported типы/функции и сигнатуры интерфейсов), а также поведение ошибок и валидации.
Ниже шаблон, который хорошо удерживает модель в рамках. Он заставляет сначала перечислить контракты, затем предложить небольшой план, а в конце дать резюме: что поменялось внутри и что не поменялось снаружи.
Ты - аккуратный Go-инженер. Задача: выполнить рефакторинг, не меняя публичные контракты.
1) СНАЧАЛА проанализируй проект и перечисли все внешние контракты, которые видишь:
- эндпоинты (method + path), коды ответов, заголовки
- JSON-структуры (поля, типы, omitempty, форматы дат)
- публичные Go-типы/функции (exported), интерфейсы и их сигнатуры
- поведение ошибок (когда 400/401/403/404/409/500, формат тела ошибки)
2) ЖЕСТКИЕ ОГРАНИЧЕНИЯ:
- Запрещено менять публичные структуры, имена полей JSON, теги, сигнатуры exported функций/методов,
а также method/path/status-коды без моего явного подтверждения.
- Запрещено менять формат ошибок, если они читаются клиентом.
- Любые вынужденные изменения контракта: остановись, объясни причину, предложи вариант без изменения.
3) План работ маленькими шагами (3-7 шагов). После каждого шага укажи проверку:
- какие тесты/команды должны пройти
- какие файлы/модули затронем
4) Выполни шаг 1. Покажи изменения как минимальный дифф.
После каждого шага: кратко напомни, что во внешнем контракте НЕ изменилось.
5) В КОНЦЕ: дифф-резюме в двух списках:
- "Изменения снаружи": должно быть "нет" (или перечисление, если я разрешил)
- "Изменения внутри": что упростили/переименовали/переместили
Контекст: (вставь сюда цель рефакторинга и файлы/пакеты)
Если вы, например, выносите общую обработку ошибок из нескольких хендлеров в один helper, такой промпт удержит ИИ от попытки «улучшить» тексты ошибок или заменить 400 на 422. Это особенно полезно, когда правки идут через чат-интерфейс, и вы хотите менять внутренности без риска для клиентов.
Рефакторинг безопаснее делать как серию маленьких, проверяемых шагов. Сначала фиксируйте цель (что именно улучшаете) и границы (что нельзя менять: пути, параметры, статусы, JSON-поля, коды ошибок, заголовки, поведение по умолчанию).
Попросите ИИ выдать план из 3-7 небольших изменений. Примеры шагов, которые обычно укладываются в «чистый» рефакторинг:
Дальше работает правило: один шаг - один набор проверок - фиксация результата. После каждого мини-коммита запускайте минимум: тесты, линтеры, сборку. Если у вас есть эталоны ответов (золотые файлы, снапшоты), сравнивайте их именно как внешний интерфейс, а не как внутренний код.
Если контракт задет, не пытайтесь «подправить по месту» и одновременно продолжать реорганизацию. Откатите шаг, уточните промпт и дайте конкретный сигнал: что именно сломалось (например, статус 400 стал 422, пропало поле error_code, изменилось имя JSON-поля), и попросите исправить только это.
В платформах, где удобно держать сценарий из «план -> маленькие правки -> проверки -> откат», такой процесс проще поддерживать дисциплинированно. В TakProsto это обычно делают через пошаговый план, снапшоты и rollback, если проверки показали расхождения.
Перед merge нужны проверки, которые ловят не только падение тестов, но и тихие изменения поведения. Лучше короткий, но обязательный набор, который прогоняется всегда.
Минимальный комплект обычно включает:
null vs поля нет»);go vet и линтеры;Типовой набор команд для CI:
go test ./... -count=1
go test ./... -race
go vet ./...
# если в проекте используется golangci-lint
# golangci-lint run
Дальше добавьте контрактный слой. Самая частая регрессия в API - не падение, а изменение формы ответа: другое имя поля, другая вложенность, другое значение по умолчанию, пустой массив вместо null или наоборот.
Практичный подход: хранить эталоны JSON-ответов для ключевых эндпоинтов (с нормализацией динамических полей вроде timestamps и UUID) и сравнивать их в тестах. Для статусов и заголовков часто хватает табличных тестов: входные данные -> ожидаемый статус, набор полей, формат ошибки.
Если API-пакеты используют другие сервисы, добавьте хотя бы «компиляционный» тест потребителя внутри репозитория: небольшой пакет, который импортирует ваши exported типы и использует их так, как это делают реальные клиенты.
После рефакторинга чаще всего всплывают не проблемы «большой логики», а несовпадения в деталях, на которые завязаны клиенты. ИИ особенно любит «подчищать» формат ошибок, формат JSON и поведение по таймаутам.
Самый частый тихий перелом - ошибки. Клиенты часто парсят не только HTTP-код, но и поля тела ответа. Для каждого типового кейса (401, 403, 404, 409, 422, 500) зафиксируйте эталон и сравнивайте структуру: поля, регистр, наличие машинного кода, стабильные значения.
Не менее коварен JSON-контракт: типы (число стало строкой), enum-значения и разница между null и отсутствием поля. На мобильных клиентах это может быть критично.
Набор регрессионных проверок, который обычно окупается быстро:
context.Context и дедлайнами;Отдельно про таймауты и контексты: после «красивого» рефакторинга иногда теряется проброс context.Context в БД/HTTP-клиенты. Под нагрузкой сервис начинает держать соединения дольше, чем раньше. Минимальная защита - интеграционный тест с маленьким timeout, который проверяет, что обработчик завершается управляемо и не зависает.
Типичная ситуация: в Go-API накопился повтор. В каждом хендлере проверяются права, дергается база, формируются похожие ошибки. Хочется вынести бизнес-логику в сервисный слой, а хендлеры сделать тонкими.
Опасность тут не в идее, а в контракте. При переписывании структур под сервисный слой легко «по пути» поменять JSON: добавить omitempty, переименовать теги, заменить int на int64 или начать возвращать null вместо отсутствующего поля.
Чтобы не гадать, выберите 3-5 запросов, которые реально используют клиенты, и зафиксируйте ответы до рефакторинга. Лучше брать кейсы с углами: ошибки, пустые списки, частичные данные.
GET /v1/profile для обычного пользователяGET /v1/profile для пользователя без части данныхPOST /v1/orders валидный запросPOST /v1/orders с ошибкой валидацииGET /v1/orders?limit=1 (проверить пагинацию и формат списка)Снимите эталон и сравните результат после изменений.
curl -s -H 'Authorization: Bearer TOKEN' https://api.example/v1/profile | jq -S . > before_profile.json
# после изменений
curl -s -H 'Authorization: Bearer TOKEN' https://api.example/v1/profile | jq -S . > after_profile.json
diff -u before_profile.json after_profile.json
Если diff показывает изменения, сначала отфильтруйте «шум»: даты, id, порядок элементов в массивах. Но если поменялись названия или наличие полей, статус-коды или формат ошибок, это почти всегда сломанный контракт.
Когда Claude Code (или любой ИИ) случайно «улучшает» контракт, не ограничивайтесь просьбой «верни как было». Дайте точные ограничения:
omitempty, имена полей, типы и формат ошибок. Исправь только внутреннюю структуру кода».error.code».Итог должен выглядеть скучно, но правильно: вы вынесли общий код в сервисы, уменьшили хендлеры, улучшили читаемость, а для клиентов API ничего не изменилось.
Главная причина поломок - слишком широкий запрос. Формулировка «улучши код» почти гарантирует попытку «навести красоту» там, где на самом деле контракт: JSON-формы, коды ошибок, тексты, статусы.
Вторая ловушка - менять публичные структуры ради читабельности. Переименовали поле в struct, удалили «неиспользуемое», добавили omitempty «потому что так принято», поменяли int на int64 или time.Time на строку. Для клиента это уже breaking change.
Отдельная боль - null и «поля нет». Например, раньше "middle_name": null означало «неизвестно», а отсутствие middle_name - «не применимо». После «аккуратной чистки» добавили omitempty, и поле исчезло. Клиентская логика ломается тихо, особенно если вы не сравниваете реальные ответы.
Еще один риск - смешивать рефакторинг и новые фичи в одном PR. ИИ легко «додумывает»: добавляет параметр, усиливает валидацию, меняет дефолты, переписывает тексты ошибок. После этого сложно понять, что вызвало регрессию.
Полезные правила, чтобы не попадать в эти ловушки:
Перед рефакторингом проверьте одну вещь: вы точно знаете, что снаружи должно остаться без изменений. Если контракт не зафиксирован, вы будете спорить не о качестве кода, а о том, «как раньше работало».
Если что-то из этого не выполнено, дешевле остановиться и сначала зафиксировать контракт, чем ловить тихую регрессию в проде.
Самый надежный вариант - превратить процесс в рутину: один шаблон промпта, один шаблон PR и одни и те же проверки в CI. Договоритесь, какие эндпоинты считаются публичными и требуют эталонов.
Если вы делаете рефакторинг через чат-интерфейс, удобно использовать инструменты с режимом планирования и быстрым откатом. Например, в TakProsto (takprosto.ai) можно сначала согласовать план, затем фиксировать состояние снапшотами и откатываться, если проверки показали расхождения в ответах.
Контракт — это все наблюдаемое поведение для клиента: путь и метод, формат JSON (поля, типы, вложенность), статус-коды, структура ошибок, важные заголовки и поведение по умолчанию.
Если после изменений клиенту нужно править код, значит контракт вы задели — даже если «логика стала правильнее».
Чаще всего ломают мелочи на границе:
user_id → userId);omitempty и смена поведения «null vs поля нет»;200 с ошибкой в теле → 404);ИИ особенно склонен «нормализовать» это как «улучшение».
Потому что модель оптимизирует читаемость и «правильность» кода, а не совместимость.
Она легко:
400 → 422),Если вы заранее не запретили менять внешнее поведение, для модели это выглядит допустимым рефакторингом.
Самый быстрый минимум:
Content-Type, Cache-Control, Request-Id, CORS);Этого достаточно, чтобы сравнить «до/после».
Считайте контрактом не код, а фактический HTTP-ответ.
Практично:
Так вы ловите изменения формы ответа, даже если тесты «зеленые».
Внутренние изменения почти всегда безопаснее:
Риск начинается там, где есть вход/выход HTTP-хендлеров: DTO, JSON-теги, middleware, ошибки, сериализация времени/чисел.
Если раньше было "field": null, а после рефакторинга поле исчезло из JSON (или наоборот), клиент может изменить поведение.
Типичный сценарий:
null) от «не применимо» (поля нет);omitempty неожиданно убирает поле;[] и null тоже часто имеют разный смысл.Зафиксируйте эти случаи в эталонных ответах и тестах, иначе регрессия будет тихой.
Держите его максимально конкретным и «запрещающим». Минимум:
Так вы снижаете шанс «косметики», которая ломает клиентов.
Базовый набор перед merge:
go test ./... -count=1 (и по возможности -race);go vet ./... и линтеры;Главная цель — поймать не падение, а отличие «ответ стал другим».
Сразу остановиться и сузить задачу:
Не смешивайте «починку контракта» и дальнейший рефакторинг в одном заходе — иначе вы потеряете причину расхождения.