Claude Code для scaffold’а Go API: зафиксируйте единый паттерн handler-service-error, затем генерируйте новые endpoint’ы, которые будут согласованными по всему Go API.

Go API обычно стартуют чисто: несколько endpoint’ов, один-два человека, и всё живёт в головах. Затем API растёт, функции доставляются под давлением, и мелкие различия прокрадываются. Каждое из них кажется безобидным, но вместе они замедляют любые будущие изменения.
Типичный пример: один handler декодирует JSON в структуру и возвращает 400 с понятным сообщением, другой возвращает 422 с другой формой ответа, а третий логирует ошибки в другом формате. Ничто из этого не ломает компиляцию. Но всё это создаёт постоянную необходимость принимать решения и переписывать мелочи при добавлении нового кода.
Вы почувствуете беспорядок в таких местах как:
CreateUser, AddUser, RegisterUser), из‑за чего сложнее искать.Под «scaffolding» здесь понимается повторяемый шаблон для новой работы: куда идёт код, что делает каждый слой и как выглядят ответы. Это не столько о генерации тонны кода, сколько о фиксации согласованной структуры.
Инструменты вроде Claude могут помочь быстро scaffold’ить новые endpoint’ы, но они полезны только если вы воспринимаете паттерн как правило. Вы определяете правила, ревьюите каждый дифф и запускаете тесты. Модель заполняет стандартные части; она не должна переопределять вашу архитектуру.
Go API остаётся простым в росте, когда каждый запрос идёт по одному и тому же пути. Перед тем как генерировать endpoint’ы, выберите одно разделение слоёв и придерживайтесь его.
Работа handler’а — только HTTP: прочитать запрос, вызвать сервис и записать ответ. В нём не должно быть бизнес-правил, SQL или логики «только для этого случая».
Сервис отвечает за use case: бизнес-правила, решения и оркестрацию между репозиториями или внешними вызовами. Он не должен знать про HTTP‑вопросы вроде статус-кодов, заголовков или того, как рендерятся ошибки.
Доступ к данным (репозиторий/стор) отвечает за детали персистенции. Он переводит намерение сервиса в SQL/запросы/транзакции. Он не должен навязывать бизнес-правила за пределами базовой целостности данных и не должен формировать API-ответы.
Контрольный список разделения, который остаётся практичным:
Выберите одно правило и не отступайте.
Простой подход:
Пример: handler проверяет, что email присутствует и похож на email. Сервис проверяет, что email разрешён и ещё не используется.
Решите заранее, возвращают ли сервисы доменные типы или DTO.
Чистый дефолт: handler’ы используют request/response DTO, сервисы — доменные типы, а handler маппит доменный тип в response. Это держит сервис стабильным, даже если HTTP-контракт меняется.
Если маппинг кажется тяжёлым, всё равно держите это последовательно: пусть сервис возвращает доменный тип плюс типизированную ошибку, а JSON‑формирование остаётся в handler’е.
Если вы хотите, чтобы сгенерированные endpoint’ы выглядели так, будто их писал один человек, зафиксируйте формат ошибок как можно раньше. Генерация работает лучше, когда формат вывода не обсуждаем: одна JSON‑форма, одна карта статус-кодов и одно правило по тому, что можно показывать клиенту.
Начните с единого «конверта» ошибки, который возвращают все endpoint’ы при неудаче. Держите его маленьким и предсказуемым:
{
"code": "validation_failed",
"message": "One or more fields are invalid.",
"details": {
"fields": {
"email": "must be a valid email address",
"age": "must be greater than 0"
}
},
"request_id": "req_01HR..."
}
Используйте code для машин (стабильный и предсказуемый) и message для людей (краткое и безопасное). Помещайте опциональные структурированные данные в details. Для валидации простая карта details.fields удобна для генерации и отображения в клиентах рядом с полями формы.
Далее зафиксируйте карту статус-кодов и придерживайтесь её. Чем меньше споров по каждому endpoint’у, тем лучше. Если вы хотите 400 и 422, сделайте разделение явным:
bad_json -> 400 Bad Request (неверный JSON)validation_failed -> 422 Unprocessable Content (корректный JSON, неверные поля)not_found -> 404 Not Foundconflict -> 409 Conflict (дубликат, рассогласование версий)unauthorized -> 401 Unauthorizedforbidden -> 403 Forbiddeninternal -> 500 Internal Server ErrorРешите, что логировать, а что возвращать. Хорошее правило: клиент получает безопасное сообщение и request_id; в логах — полный стек и внутренний контекст (SQL, входящие payload’ы upstream, ID пользователей), которые нельзя показывать клиенту.
Наконец, стандартизируйте request_id. Принимайте входной заголовок с ID, если он есть (от API‑gateway), иначе генерируйте его на краю (middleware). Кладите его в context, включайте в логи и возвращайте в каждом ответе об ошибке.
Если вы хотите, чтобы scaffolding оставался согласованным, структура папок должна быть скучной и повторяемой. Генераторы следуют шаблонам, которые видны в проекте, но дрейф возникает, когда файлы разбросаны или имена меняются в зависимости от фичи.
Выберите одну конвенцию нейминга и не отступайте. Выберите одно слово для каждого понятия и придерживайтесь его: handler, service, repo, request, response. Если маршрут — POST /users, называйте файлы и типы вокруг users и create (а не иногда register, иногда addUser).
Простая структура, соответствующая распространённым слоям:
internal/
httpapi/
handlers/
users_handler.go
services/
users_service.go
data/
users_repo.go
apitypes/
users_types.go
Решите, где лежат общие типы — это именно то место, где проекты часто путаются. Одно полезное правило:
internal/apitypes (соответствуют JSON и требованиям валидации).Если тип имеет JSON-тэги и предназначен для клиентов, обращайтесь с ним как с API-типом.
Держите зависимости handler’ов минимальными и сформулируйте это явно:
Напишите короткий паттерн-док в корне репозитория (обычный Markdown подходит). Включите дерево папок, правила нейминга и один маленький пример потока (handler -> service -> repo и в каких файлах что лежит). Это та самая точная справка, которую вы вставляете в генератор, чтобы новые endpoint’ы соответствовали структуре каждый раз.
Перед тем как генерировать десять endpoint’ов, сделайте один, которому вы доверяете. Это эталон: файл, на который вы указываете и говорите «Новый код должен выглядеть так». Можно написать его с нуля или отрефакторить существующий, пока он не станет соответствовать правилам.
Держите handler тонким. Один приём, который сильно помогает: положите интерфейс между handler’ом и сервисом, чтобы handler зависел от контракта, а не от конкретной структуры.
Добавьте небольшие комментарии в эталонном endpoint’е только там, где генерируемый код может запутаться. Объясните решения (почему 400 vs 422, почему create возвращает 201, зачем скрывать внутренние ошибки за общим сообщением). Пропустите комментарии, которые просто пересказывают код.
Когда эталонный endpoint работает, извлеките хелперы, чтобы у каждого нового endpoint’а было меньше шансов уйти в дрейф. Самые переиспользуемые хелперы обычно:
Вот как «тонкий handler + интерфейс» может выглядеть на практике:
type UserService interface {
CreateUser(ctx context.Context, in CreateUserInput) (User, error)
}
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
var in CreateUserRequest
if err := BindJSON(r, &in); err != nil {
WriteError(w, ErrBadJSON) // 400: malformed JSON
return
}
if err := Validate(in); err != nil {
WriteError(w, err) // 422: validation details
return
}
user, err := h.svc.CreateUser(r.Context(), in.ToInput())
if err != nil {
WriteError(w, err)
return
}
WriteJSON(w, http.StatusCreated, user)
}
Закрепите это парой тестов (хотя бы небольшой таблицей тестов для маппинга ошибок). Генерация работает лучше, когда у неё есть одна чистая цель для имитации.
Согласованность начинается с того, что вы вставляете и что запрещаете. Для нового endpoint’а дайте две вещи:
Включите handler, метод сервиса, request/response типы и любые общие хелперы, которые использует endpoint. Затем пропишите контракт простыми словами:
POST /v1/widgets)Будьте явны в том, что должно совпадать: нейминги, пути пакетов и имена хелперов (WriteJSON, BindJSON, WriteError, ваш валидатор).
Жёсткая подсказка предотвращает «полезные» рефакторинги. Например:
Using the reference endpoint below and the pattern notes, generate a new endpoint.
Contract:
- Route: POST /v1/widgets
- Request: {"name": string, "color": string}
- Response: {"id": string, "name": string, "color": string, "createdAt": string}
- Errors: invalid JSON -> 400; validation -> 422; duplicate name -> 409; unexpected -> 500
Output ONLY these files:
1) internal/http/handlers/widgets_create.go
2) internal/service/widgets.go (add method only)
3) internal/types/widgets.go (add types only)
Do not change: router setup, existing error format, existing helpers, or unrelated files.
Must use: package paths and helper functions exactly as in the reference.
Если используете тесты — запрашивайте их явно (и указывайте имя тест‑файла). Иначе модель может пропустить тесты или придумать собственный тестовый сетап.
Сделайте быструю проверку диффа после генерации. Если сгенерированный код изменил общие хелперы, регистрацию роутера или стандартный формат ошибок — отклоняйте и жёстче повторяйте правила «не менять».
Выход будет таким же последовательным, как и вход. Быстрее всего избежать «почти правильного» кода — каждый раз переиспользовать одну шаблонную подсказку с небольшим фрагментом контекста из вашего репозитория.
Скопируйте, вставьте и заполните плейсхолдеры:
You are editing an existing Go HTTP API.
CONTEXT
- Folder tree (only the relevant parts):
<paste a small tree: internal/http, internal/service, internal/repo, etc>
- Key types and patterns:
- Handler signature style: <example>
- Service interface style: <example>
- Request/response DTOs live in: <package>
- Standard error response JSON:
{
"error": {
"code": "invalid_argument",
"message": "...",
"details": {"field": "reason"}
}
}
- Status code map:
invalid_json -> 400
invalid_argument -> 422
not_found -> 404
conflict -> 409
internal -> 500
TASK
Add a new endpoint: <METHOD> <PATH>
- Handler name: <Name>
- Service method: <Name>
- Request JSON example:
{"name":"Acme"}
- Success response JSON example:
{"id":"123","name":"Acme"}
CONSTRAINTS
- No new dependencies.
- Keep functions small and single-purpose.
- Match existing naming, folder layout, and error style exactly.
- Do not refactor unrelated files.
ACCEPTANCE CHECKS
- Code builds.
- Existing tests pass (add tests only if the repo already uses them for handlers/services).
- Run gofmt on changed files.
FINAL INSTRUCTION
Before writing code, list any assumptions you must make. If an assumption is risky, ask a short question instead.
Это работает, потому что заставляет три вещи: блок контекста (что уже есть), блок ограничений (что нельзя делать) и конкретные JSON‑примеры (чтобы формы не ушли в дрейф). Финальная инструкция — это предохранитель: если модель не уверена, она должна спросить, прежде чем вносить изменения.
Допустим, вы хотите добавить endpoint «Create project». Цель проста: принять имя, применить пару правил, сохранить и вернуть новый ID. Сложность в том, чтобы сохранить разделение handler-service-repo и формат ошибок, который вы уже используете.
Согласованный поток выглядит так:
Вот запрос, который принимает handler:
{ "name": "Roadmap", "owner_id": "u_123" }
Успех — 201 Created. ID должен приходить из одного места всегда. Например, пусть Postgres его генерирует, а репозиторий возвращает:
{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }
Два реалистичных пути неудачи:
Если валидация не проходит (имя отсутствует или слишком короткое), верните полевую ошибку в вашем стандартном формате и выбранный статус-код:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }
Если имя должно быть уникально для владельца и сервис обнаруживает уже существующий проект, верните 409 Conflict:
{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }
Одно решение, которое упрощает паттерн: handler проверяет «правильна ли форма запроса?», а сервис владеет вопросом «разрешено ли это?». Такое разделение делает сгенерированные endpoint’ы предсказуемыми.
Самый быстрый путь потерять согласованность — позволить генератору импровизировать.
Один из дрейфов — новая форма ошибки. Один endpoint возвращает {error: "..."}, другой {message: "..."}, третий добавляет вложенный объект. Исправляйте это, сохранив один конверт ошибок и одну карту статус-кодов в одном месте, затем требуйте, чтобы новые endpoint’ы переиспользовали их по пути импорта и имени функции. Если генератор предлагает новое поле — рассмотрите это как запрос на изменение API, а не как удобство.
Другой дрейф — раздутые handler’ы. Всё начинается невинно: валидировать, потом проверить права, потом запросить БД, потом ветвление по бизнес-правилам. Скоро каждый handler выглядит по‑разному. Держите правило: handler переводит HTTP в типизированные входы и выходы; сервисы владеют решениями; доступ к данным — запросами.
Несоответствие в нейминге тоже накапливается. Если один endpoint использует CreateUserRequest, а другой — NewUserPayload, вы потеряете время на поиск типов и написание glue‑кода. Выберите схему нейминга и отклоняйте новые имена без серьёзной причины.
Никогда не возвращайте сырые ошибки базы данных клиентам. Помимо утечки деталей, это создаёт несогласованные сообщения и статус-коды. Оборачивайте внутренние ошибки, логируйте причину и возвращайте стабильный публичный код.
Избегайте добавления новых библиотек «ради удобства». Каждая дополнительная валидация, хелпер роутера или пакет ошибок становится ещё одним стилем, который нужно поддерживать.
Защитные механизмы, которые предотвращают большую часть поломок:
Если вы не можете сравнить два endpoint’а и увидеть одинаковую форму (импорты, поток, обработку ошибок), ужесточайте подсказку и регенерите перед мерджем.
Перед мерджем сначала проверьте структуру. Если форма правильная, логические баги легче заметить.
Проверки структуры:
request_id.Проверки поведения:
not found или conflict) и подтвердите HTTP‑статус и форму JSON.Воспринимайте паттерн как общий контракт, а не как предпочтение. Держите документ «как мы строим endpoint’ы» рядом с кодом и поддерживайте один эталонный endpoint, который показывает полный подход от конца до конца.
Масштабируйте генерацию маленькими пачками. Генерируйте 2–3 endpoint’а, которые покрывают разные сценарии (простой чтение, create с валидацией, update с not-found). Затем остановитесь и доработайте. Если в ревью постоянно всплывает один и тот же дрейф — обновите базовый документ и эталонный endpoint прежде чем генерировать дальше.
Цикл, который можно повторять:
Если нужен более жёсткий цикл билд‑ревью, платформа vibe-coding вроде Koder.ai (koder.ai) может помочь scaffold’ить и итерировать быстро в чат‑оркестровке, затем экспортировать код, когда он совпадает с вашим стандартом. Инструмент менее важен, чем правило: ваша baseline‑документация должна оставаться главным источником истины.
Зафиксируйте повторяемый шаблон как можно раньше: единое разделение слоёв (handler → service → data access), один конверт ошибок и карта статус-кодов. Затем используйте один «эталонный» endpoint как пример, которому должны соответствовать все новые endpoint’ы.
Держите handler HTTP-ориентированным:
Если в handler’e вы видите SQL, проверки прав доступа или бизнес-ветвление — вынесите это в сервис.
Помещайте бизнес-правила и решения в сервис:
Сервис должен возвращать доменные результаты и типизированные ошибки — никаких HTTP-статусов и JSON-формирования.
Изолируйте детали персистенции:
Избегайте кодирования форматов API-ответов или навешивания бизнес-правил в репозитории — за пределами базовой целостности данных.
Простой дефолт:
Пример: handler проверяет, что email присутствует и похож на email; сервис проверяет, разрешён ли этот email и не занят ли он.
Используйте единый конверт ошибок и держите его стабильным. Практичная форма:
code для машин (стабильный)message для людей (короткое и безопасное)details для структурированных дополнительных данных (например, ошибки по полям)request_id для трассировкиЭто предотвращает раздувание специальных кейсов на стороне клиента и делает генерацию предсказуемой.
Запишите карту статус-кодов и придерживайтесь её. Частая разбивка:
Возвращайте безопасные, согласованные публичные ошибки и логируйте реальную причину внутренне.
code, короткое message, плюс request_idЭто предотвращает утечку внутренних данных и разницу в сообщениях ошибок между endpoint’ами.
Создайте один «золотой» endpoint, которому обязаны соответствовать новые:
BindJSON, WriteJSON, WriteError и т.д.)Добавьте пару небольших тестов (например, таблицу тестов для маппинга ошибок), чтобы закрепить паттерн.
Дайте модели строгий контекст и ограничения:
После генерации отклоняйте диффы, которые «улучшают» архитектуру вместо того, чтобы следовать базовому шаблону.
400 — сломанный JSON (bad_json)422 — ошибки валидации (validation_failed)404 — ресурс не найден (not_found)409 — конфликт (дубликат/версионный конфликт)500 — неожиданные ошибкиГлавное — последовательность: не обсуждать статус для каждого endpoint’а отдельно.