Claude Code para scaffolding de API Go: defina um padrão limpo handler-service-erro e gere novos endpoints que permaneçam consistentes em toda a sua API Go.

APIs Go geralmente começam limpas: alguns endpoints, uma ou duas pessoas, e tudo vive na cabeça de cada um. Depois a API cresce, funcionalidades saem com pressa, e pequenas diferenças aparecem. Cada uma parece inofensiva, mas juntas elas tornam qualquer mudança futura mais lenta.
Um exemplo comum: um handler decodifica JSON em uma struct e retorna 400 com uma mensagem útil, outro retorna 422 com um formato diferente, e um terceiro registra erros em outro formato. Nada disso quebra a compilação. Só cria decisões constantes e pequenas reescritas sempre que você adiciona algo.
Você sente a bagunça em lugares como:
CreateUser, AddUser, RegisterUser) que dificulta buscas.“Scaffolding” aqui significa um template repetível para trabalho novo: onde o código vai, o que cada camada faz e como as respostas devem parecer. É menos sobre gerar muito código e mais sobre travar uma forma consistente.
Ferramentas como Claude podem ajudar a scaffoldar novos endpoints rápido, mas só continuam úteis quando você trata o padrão como uma regra. Você define as regras, revisa cada diff e executa testes. O modelo preenche as partes padrão; ele não pode redefinir sua arquitetura.
Uma API Go fica fácil de crescer quando cada requisição segue o mesmo caminho. Antes de começar a gerar endpoints, escolha uma separação de camadas e mantenha-a.
O trabalho do handler é só HTTP: ler a requisição, chamar o service e escrever a resposta. Ele não deve conter regras de negócio, SQL nem lógica de “só este caso especial”.
O service é dono do caso de uso: regras de negócio, decisões e orquestração entre repositórios ou chamadas externas. Não deve conhecer preocupações HTTP como códigos de status, headers ou como erros são renderizados.
O acesso a dados (repository/store) é dono dos detalhes de persistência. Ele traduz a intenção do service para SQL/queries/transações. Não deve impor regras de negócio além da integridade básica dos dados, e não deve moldar respostas da API.
Uma checklist de separação prática:
Escolha uma regra e não a dobre.
Uma abordagem simples:
Exemplo: o handler verifica que email está presente e parece um email. O service verifica que o email é permitido e não está em uso.
Decida desde cedo se os services retornam tipos de domínio ou DTOs.
Um padrão limpo é: handlers usam DTOs de request/response, services usam tipos de domínio, e o handler faz o mapeamento de domínio para resposta. Isso mantém o service estável mesmo quando o contrato HTTP muda.
Se o mapeamento parecer pesado, mantenha a consistência de qualquer forma: faça o service retornar um tipo de domínio mais um erro tipado, e mantenha o formato JSON no handler.
Se você quer que endpoints gerados pareçam escritos pela mesma pessoa, trave as respostas de erro cedo. Geração funciona melhor quando o formato de saída é inegociável: um shape JSON, um mapa de status codes e uma regra sobre o que é exposto.
Comece com um envelope de erro único que todo endpoint retorna em falha. Mantenha-o pequeno e previsível:
{
"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..."
}
Use code para máquinas (estável e previsível) e message para humanos (curto e seguro). Coloque dados estruturados opcionais em details. Para validação, um simples mapa details.fields é fácil de gerar e fácil para clientes exibirem próximo aos inputs.
Em seguida, escreva um mapa de status codes e mantenha-o. Menos debate por endpoint é melhor. Se quiser ambos 400 e 422, deixe a divisão explícita:
bad_json -> 400 Bad Request (JSON malformado)validation_failed -> 422 Unprocessable Content (JSON bem formado, campos inválidos)not_found -> 404 Not Foundconflict -> 409 Conflict (chave duplicada, mismatch de versão)unauthorized -> 401 Unauthorizedforbidden -> 403 Forbiddeninternal -> 500 Internal Server ErrorDecida o que você loga vs o que você retorna. Uma boa regra: clientes recebem uma mensagem segura e um request ID; os logs recebem o erro completo e o contexto interno (SQL, payloads upstream, IDs de usuário) que você nunca quer vazar.
Por fim, padronize request_id. Aceite um header de ID se presente (vindo de um API gateway), caso contrário gere um no limite (middleware). Anexe-o ao context, inclua-o nos logs e retorne-o em toda resposta de erro.
Se você quer que o scaffolding permaneça consistente, seu layout de pastas tem que ser chato e repetível. Geradores seguem padrões que conseguem ver, mas eles se desviam quando arquivos estão espalhados ou nomes mudam por recurso.
Escolha uma convenção de nomes e mantenha-a. Escolha uma palavra para cada coisa e use sempre: handler, service, repo, request, response. Se a rota é POST /users, nomeie arquivos e tipos em torno de users e create (não às vezes register, às vezes addUser).
Um layout simples que casa com as camadas:
internal/
httpapi/
handlers/
users_handler.go
services/
users_service.go
data/
users_repo.go
apitypes/
users_types.go
Decida onde tipos compartilhados vivem, porque é aí que projetos costumam ficar bagunçados. Uma regra útil:
internal/apitypes (casam com JSON e necessidades de validação).Se um tipo tem tags JSON e é projetado para clientes, trate-o como um tipo de API.
Mantenha dependências dos handlers mínimas e faça essa regra explícita:
Escreva um documento curto de padrão na raiz do repo (Markdown simples é suficiente). Inclua a árvore de pastas, regras de nomeação e um pequeno fluxo de exemplo (handler -> service -> repo, mais em qual arquivo cada peça fica). Essa é a referência exata que você cola no gerador para que novos endpoints batam com a estrutura sempre.
Antes de gerar dez endpoints, crie um endpoint que você confie. Esse é o padrão dourado: o arquivo que você aponta e diz, “novo código deve parecer com isto.” Você pode escrevê-lo do zero ou refatorar um existente até que bata.
Mantenha o handler fino. Uma prática que ajuda bastante: coloque uma interface entre o handler e o service para que o handler dependa de um contrato, não de um struct concreto.
Adicione pequenos comentários no endpoint de referência somente onde código gerado futuro pode se confundir. Explique decisões (por que 400 vs 422, por que create retorna 201, por que você oculta erros internos atrás de uma mensagem genérica). Evite comentários que só repitam o código.
Quando o endpoint de referência funcionar, extraia helpers para que todo novo endpoint tenha menos chances de desviar. Os helpers mais reutilizáveis costumam ser:
Veja como “handler fino + interface” pode se parecer na prática:
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)
}
Trave isso com alguns testes simples (mesmo um pequeno teste de tabela para mapeamento de erros). Geração funciona melhor quando tem um alvo limpo para imitar.
Consistência começa com o que você cola e o que você proíbe. Para um endpoint novo, dê duas coisas:
Inclua o handler, método do service, tipos de request/response e quaisquer helpers compartilhados que o endpoint usa. Depois declare o contrato em termos simples:
POST /v1/widgets)Seja explícito sobre o que deve bater: nomes, paths de pacote e funções helper (WriteJSON, BindJSON, WriteError, seu validador).
Um prompt apertado previne “refatorações úteis”. Por exemplo:
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.
Se você usar testes, solicite-os explicitamente (e nomeie o arquivo de teste). Caso contrário o modelo pode pular os testes ou inventar uma configuração. Faça um quick diff após a geração. Se modificou helpers compartilhados, registro do router ou o formato padrão de erro, rejeite a saída e reforce as regras de “não mudar”.
A saída é tão consistente quanto a entrada. A forma mais rápida de evitar código “quase certo” é reutilizar um template de prompt toda vez, com um pequeno snapshot de contexto do seu repo.
Cole, preencha e substitua os placeholders:
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.
Isso funciona porque força três coisas: um bloco de contexto (o que existe), um bloco de restrições (o que não fazer) e exemplos JSON concretos (para que os shapes não mudem). A instrução final é a sua rede de segurança: se o modelo estiver incerto, ele deve dizer antes de se comprometer a criar código.
Suponha que você queira adicionar um endpoint “Create project”. O objetivo é simples: aceitar um nome, aplicar algumas regras, armazenar e retornar um novo ID. O complicado é manter a mesma separação handler-service-repo e o mesmo JSON de erro que você já usa.
Um fluxo consistente parece assim:
Aqui está a requisição que o handler aceita:
{ "name": "Roadmap", "owner_id": "u_123" }
No sucesso, retorne 201 Created. O ID deve vir de um lugar sempre. Por exemplo, deixe o Postgres gerar e faça o repo retorná-lo:
{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }
Dois caminhos de falha realistas:
Se a validação falhar (nome faltando ou curto demais), retorne um erro por campo usando seu shape padrão e o status escolhido:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }
Se o nome precisa ser único por owner e o service encontrar um projeto existente, retorne 409 Conflict:
{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }
Uma decisão que mantém o padrão limpo: o handler checa “esta requisição tem a forma correta?” enquanto o service é dono de “isto é permitido?”. Essa separação torna endpoints gerados previsíveis.
A maneira mais rápida de perder consistência é deixar o gerador improvisar.
Uma deriva comum é um novo shape de erro. Um endpoint retorna {error: "..."}, outro retorna {message: "..."}, e um terceiro adiciona um objeto aninhado. Corrija isso mantendo um envelope de erro único e um mapa de códigos de status em um lugar, e exigindo que novos endpoints os reutilizem por import path e nome de função. Se o gerador propuser um novo campo, trate como uma mudança de API, não uma conveniência.
Outra deriva é o inchamento do handler. Começa pequeno: validar, depois checar permissões, depois consultar o DB, depois ramificar em regras de negócio. Logo cada handler parece diferente. Mantenha uma regra: handlers traduzem HTTP para inputs/outputs tipados; services são donos das decisões; acesso a dados é dono das queries.
Desalinhamentos de nomes também se acumulam. Se um endpoint usa CreateUserRequest e outro NewUserPayload, você perde tempo caçando tipos e escrevendo glue. Escolha um esquema de nomes e rejeite novos nomes, salvo razão forte.
Nunca retorne erros brutos do banco para clientes. Além de vazar detalhes, cria mensagens e códigos inconsistentes. Envolva erros internos, logue a causa e retorne um código público estável.
Evite adicionar bibliotecas só “por conveniência”. Cada validador, helper de router ou pacote de erro extra vira outro estilo para igualar.
Guardrails que evitam a maior parte dos problemas:
Se você não consegue diffar dois endpoints e ver a mesma forma (imports, fluxo, tratamento de erro), aperte o prompt e regenere antes de fazer merge.
Antes de mesclar algo gerado, cheque a estrutura primeiro. Se a forma estiver certa, bugs lógicos ficam mais fáceis de achar.
Checagens de estrutura:
request_id.Checagens de comportamento:
Trate seu padrão como um contrato compartilhado, não uma preferência. Mantenha o documento “como construímos endpoints” ao lado do código, e mantenha um endpoint de referência que mostra a abordagem completa end-to-end.
Escalone a geração em pequenos lotes. Gere 2 a 3 endpoints que atinjam diferentes bordas (um read simples, um create com validação, um update com caso de not-found). Depois pare e refine. Se revisões continuarem encontrando as mesmas derivações de estilo, atualize o documento de base e o endpoint de referência antes de gerar mais.
Um loop que você pode repetir:
Se quiser um ciclo de build-review mais apertado, uma plataforma vibe-coding como Koder.ai (koder.ai) pode ajudar a scaffoldar e iterar rápido em um fluxo de chat, depois exportar o código fonte quando bater com seu padrão. A ferramenta importa menos que a regra: sua baseline continua no comando.
Trave um template repetível cedo: uma divisão de camadas consistente (handler → service → data access), um envelope de erro único e um mapa de códigos de status. Depois, use um único “endpoint de referência” como exemplo que todo novo endpoint deve copiar.
Mantenha os handlers apenas com responsabilidades HTTP:
Se você ver SQL, verificações de permissão ou ramificações de negócio no handler, mova isso para o service.
Coloque regras de negócio e decisões no service:
O service deve retornar resultados de domínio e erros tipados—sem códigos HTTP e sem formatação JSON.
Mantenha as preocupações de persistência isoladas:
Evite codificar formatos de resposta da API ou impor regras de negócio no repo, além da integridade básica dos dados.
Um padrão simples:
Exemplo: o handler verifica que email está presente e parece um email; o service verifica que ele é permitido e não está em uso.
Use um único envelope de erro em toda a API e mantenha-o estável. Uma forma prática:
code para máquinas (estável)message para humanos (curto e seguro)details para extras estruturados (ex.: erros por campo)request_id para rastreioIsso evita casos especiais no cliente e torna endpoints gerados previsíveis.
Escreva um mapa de códigos de status e siga-o sempre. Uma divisão comum:
Retorne erros públicos seguros e consistentes, e registre a causa real internamente.
code estável, message curto e request_idIsso evita vazamento de informações internas e diferenças aleatórias nas mensagens de erro entre endpoints.
Crie um endpoint “golden” que você confia e exija que novos endpoints o reproduzam:
BindJSON, WriteJSON, WriteError, etc.)Adicione alguns testes pequenos (mesmo testes de tabela para mapeamento de erros) para fixar o padrão.
Dê ao modelo contexto e restrições estritas:
Após a geração, rejeite diffs que “melhorem” a arquitetura em vez de seguir a linha base.
400 para JSON malformado (bad_json)422 para falhas de validação (validation_failed)404 para recursos não encontrados (not_found)409 para conflitos (duplicatas/versões)500 para falhas inesperadasO importante é consistência: nada de debates por endpoint.