Claude Code para scaffolding de API Go: handlers e services consistentes
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.

Por que APIs Go ficam confusas quando padrões não são definidos cedo
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:
- Corpos de erro diferentes entre endpoints, então clientes precisam de casos especiais.
- Services que às vezes retornam erros brutos do banco e às vezes erros “amigáveis”.
- Deriva de nomes (
CreateUser,AddUser,RegisterUser) que dificulta buscas. - Validação que fica espalhada, fazendo com que os mesmos bugs retornem.
“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.
Escolha uma separação de camadas: handler, service e acesso a dados
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.
Responsabilidades, mantidas simples
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:
- Handler: parsear input, chamar service, mapear erros para HTTP, escrever JSON
- Service: aplicar regras de negócio, chamar repos, retornar resultados de domínio ou erros tipados
- Acesso a dados: executar consultas, mapear linhas para structs, retornar erros de armazenamento
- Compartilhado: tipos de erro comuns e helpers de resposta
- Nenhuma camada pula outra (handler nunca chama repo diretamente)
Uma regra para validação
Escolha uma regra e não a dobre.
Uma abordagem simples:
- Handlers fazem “validação de forma” (campos obrigatórios, formato básico).
- Services fazem “validação de significado” (permissões, invariantes, estado).
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.
O que se move entre camadas
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.
Defina uma resposta de erro padrão e um mapa de códigos de status
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 Error
Decida 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.
Layout de pastas e nomes que tornam a geração previsível
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:
- Tipos de request/response da API vivem em
internal/apitypes(casam com JSON e necessidades de validação). - Tipos de domínio vivem mais próximos da camada de service (casam com regras de negócio).
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:
- Handlers importam apenas: routing/http, context, apitypes e services
- Services importam: tipos de domínio e acesso a dados
- Acesso a dados importa: driver do banco e helpers de query
- Nenhum handler importa o pacote do banco diretamente
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.
Crie um endpoint de referência que defina o padrão
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:
- Bind JSON e lidar com bodies malformados
- Validar input e retornar erros por campo
- Escrever respostas JSON de forma consistente
- Mapear erros de domínio para códigos HTTP
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.
Passo a passo: peça ao Claude para gerar um endpoint que bate com o padrão
Consistência começa com o que você cola e o que você proíbe. Para um endpoint novo, dê duas coisas:
- Seu endpoint de referência (o exemplo “perfeito”)
- Uma nota curta de padrão que nomeia pacotes, funções e helpers que devem ser usados
1) Forneça contexto primeiro (referência + regras)
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:
- Rota + método (exemplo:
POST /v1/widgets) - Campos do JSON de request (obrigatórios vs opcionais)
- Shape do JSON de response
- Casos de erro e códigos de status
- Arquivos que você espera de volta (e somente esses arquivos)
Seja explícito sobre o que deve bater: nomes, paths de pacote e funções helper (WriteJSON, BindJSON, WriteError, seu validador).
2) Peça a saída no formato exato que você quer
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”.
Um template de prompt reutilizável para scaffolds consistentes
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.
Template de prompt
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.
Exemplo realista: adicionar um endpoint Create sem quebrar o estilo
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:
- Handler: bind JSON, validar campos básicos, chamar o service
- Service: aplicar regras de negócio (como unicidade), chamar o repo, retornar um resultado de domínio
- Repo: gravar no Postgres, retornar o ID gerado
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.
Erros comuns que quebram a consistência (e como evitá-los)
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:
- Exigir que novos endpoints reutilizem tipos de erro e helpers existentes.
- Manter handlers sem regras de negócio e sem acesso ao banco.
- Aplicar uma convenção única de nomes para request/response structs.
- Mapear erros internos para códigos públicos, nunca retornar erros brutos.
- Adicionar dependências apenas com uma justificativa escrita clara.
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.
Checklist rápido antes de mesclar um endpoint gerado
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:
- Fluxo do handler consistente: bind input, validar, chamar service, mapear erros de domínio para HTTP, escrever resposta.
- Código do service com regras de negócio apenas: nada de HTTP ou JSON.
- Respostas de sucesso batem com o estilo da casa (envelope compartilhado ou JSON direto).
- Respostas de erro uniformes: mesmos campos JSON, mesmos códigos, mesmo comportamento de
request_id. - Nomes e localizações monótonos: nomes de arquivo, funções e rotas batem com endpoints existentes, e tudo está gofmt-formatted.
Checagens de comportamento:
- Rode testes e adicione pelo menos um teste pequeno para um novo branch do handler/service.
- Confirme que falhas de validação retornam mesmo código e status que endpoints semelhantes.
- Dispare um erro de service conhecido (ex.: "not found" ou "conflict") e confirme status HTTP e shape JSON.
- Procure por restos de copy-paste: rota errada, mensagem de log errada, nomes de DTOs inconsistentes.
- Compile e rode o servidor localmente ao menos uma vez para garantir wiring e imports.
Próximos passos: padronize o padrão, depois escale geração com segurança
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:
- Escreva a base: nomes de arquivos, nomes de funções, structs de request/response, códigos de erro e onde a validação acontece.
- Mantenha um endpoint "golden" e atualize-o sempre que o padrão mudar.
- Gere em batch alguns endpoints, revise pela consistência e ajuste o prompt e o doc de padrão.
- Refatore endpoints antigos em blocos e mantenha um caminho de rollback se comportamento mudar.
- Monitore uma métrica por uma semana: tempo para adicionar um endpoint, taxa de bugs após merge ou tempo de revisão.
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.
Perguntas frequentes
What’s the fastest way to stop a Go API from getting inconsistent?
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.
What should a handler do (and not do)?
Mantenha os handlers apenas com responsabilidades HTTP:
- Fazer bind/parse da requisição
- Fazer validação de “forma” (campos obrigatórios, formatos simples)
- Chamar o service
- Mapear erros tipados para códigos HTTP
- Escrever JSON usando helpers compartilhados
Se você ver SQL, verificações de permissão ou ramificações de negócio no handler, mova isso para o service.
What belongs in the service layer?
Coloque regras de negócio e decisões no service:
- Permissões e regras de acesso
- Invariantes (transições de estado, unicidade, checagens de "permitido")
- Orquestração entre repositórios e chamadas externas
O service deve retornar resultados de domínio e erros tipados—sem códigos HTTP e sem formatação JSON.
What belongs in the data access/repository layer?
Mantenha as preocupações de persistência isoladas:
- SQL/queries e transações
- Mapear linhas para structs
- Retornar erros de armazenamento (para o service interpretar)
Evite codificar formatos de resposta da API ou impor regras de negócio no repo, além da integridade básica dos dados.
Where should validation live?
Um padrão simples:
- Handlers validam a forma da requisição (campos faltando, formatos básicos)
- Services validam o significado (permissões, invariantes, estado)
Exemplo: o handler verifica que email está presente e parece um email; o service verifica que ele é permitido e não está em uso.
What should a standard API error response look like?
Use um único envelope de erro em toda a API e mantenha-o estável. Uma forma prática:
codepara máquinas (estável)messagepara humanos (curto e seguro)detailspara extras estruturados (ex.: erros por campo)request_idpara rastreio
Isso evita casos especiais no cliente e torna endpoints gerados previsíveis.
How do I choose between 400 vs 422 vs 409 for errors?
Escreva um mapa de códigos de status e siga-o sempre. Uma divisão comum:
400para JSON malformado (bad_json)422para falhas de validação (validation_failed)404para recursos não encontrados (not_found)409para conflitos (duplicatas/versões)500para falhas inesperadas
O importante é consistência: nada de debates por endpoint.
Should I ever return raw database errors to clients?
Retorne erros públicos seguros e consistentes, e registre a causa real internamente.
- Resposta:
codeestável,messagecurto erequest_id - Logs: detalhes completos do erro (erros SQL, payloads upstream, IDs de usuário)
Isso evita vazamento de informações internas e diferenças aleatórias nas mensagens de erro entre endpoints.
What is a reference endpoint, and why do I need one?
Crie um endpoint “golden” que você confia e exija que novos endpoints o reproduzam:
- Mesmo fluxo (bind → validate → service → map de erros → JSON)
- Mesmos helpers (
BindJSON,WriteJSON,WriteError, etc.) - Mesma organização de pastas e nomes
Adicione alguns testes pequenos (mesmo testes de tabela para mapeamento de erros) para fixar o padrão.
How do I prompt Claude to generate new endpoints without breaking my structure?
Dê ao modelo contexto e restrições estritas:
- Cole o endpoint de referência e as regras do padrão
- Especifique rota, exemplos de JSON de request/response e casos de erro
- Liste exatamente quais arquivos ele pode gerar
- Diga explicitamente o que não deve mudar (setup do router, formato de erro, helpers)
Após a geração, rejeite diffs que “melhorem” a arquitetura em vez de seguir a linha base.