Claude Code per lo scaffolding delle API Go: definisci un pattern chiaro handler-service-error, poi genera nuovi endpoint che rimangono coerenti attraverso la tua API Go.

Le API Go di solito partono pulite: pochi endpoint, una o due persone, e tutto vive nella testa di qualcuno. Poi l’API cresce, le feature vengono consegnate sotto pressione e piccole differenze si insinuano. Ognuna sembra innocua, ma insieme rallentano ogni cambiamento futuro.
Un esempio comune: un handler decodifica JSON in una struct e ritorna 400 con un messaggio utile, un altro restituisce 422 con una forma diversa, e un terzo logga gli errori in un formato differente. Niente di tutto questo rompe la compilazione. Crea invece decisioni costanti e riscritture piccole ogni volta che aggiungi qualcosa.
La confusione si percepisce in posti come:
CreateUser, AddUser, RegisterUser) che rende le ricerche più difficili.Per “scaffolding” qui intendo un template ripetibile per il lavoro nuovo: dove va il codice, cosa fa ogni livello e che aspetto hanno le risposte. È meno generare molto codice e più fissare una forma coerente.
Strumenti come Claude possono aiutare a scaffoldare nuovi endpoint velocemente, ma restano utili solo se tratti il pattern come una regola. Tu definisci le regole, revisioni ogni diff e esegui i test. Il modello riempie le parti standard; non può ridefinire la tua architettura.
Un’API Go resta facile da far crescere quando ogni richiesta segue lo stesso percorso. Prima di generare endpoint, scegli una divisione dei livelli e mantienila.
Il lavoro dell’handler è solo HTTP: leggere la richiesta, chiamare il service e scrivere la risposta. Non dovrebbe contenere regole di business, SQL o logiche del tipo “solo questo caso speciale”.
Il service possiede il caso d’uso: regole di business, decisioni e orchestrazione tra repository o chiamate esterne. Non dovrebbe conoscere preoccupazioni HTTP come status code, header o come vengono renderizzati gli errori.
L’accesso ai dati (repository/store) possiede i dettagli di persistenza. Traduce l’intento del service in SQL/query/transaction. Non dovrebbe imporre regole di business oltre l’integrità dei dati di base né modellare le risposte API.
Una checklist di separazione pratica:
Scegli una regola e non piegarla.
Un approccio semplice:
Esempio: l’handler verifica che email sia presente e abbia formato email. Il service verifica che l’email sia consentita e non già in uso.
Decidi presto se i service restituiscono tipi di dominio o DTO.
Un default pulito è: gli handler usano request/response DTO, i service usano tipi di dominio, e l’handler mappa il dominio nella response. Questo mantiene stabile il service anche quando il contratto HTTP cambia.
Se il mapping sembra pesante, mantienilo coerente comunque: fai restituire al service un tipo di dominio più un errore tipato, e tieni il shaping JSON nell’handler.
Se vuoi che gli endpoint generati sembrino scritti dalla stessa persona, fissa le risposte di errore presto. La generazione funziona meglio quando il formato di output non è negoziabile: un solo schema JSON, una sola mappa di status code e una regola su cosa viene esposto.
Inizia con un singolo involucro di errore che ogni endpoint restituisce in caso di fallimento. Mantienilo piccolo e prevedibile:
{
"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..."
}
Usa code per le macchine (stabile e prevedibile) e message per gli umani (breve e sicuro). Metti dati strutturati opzionali in details. Per la validazione, una semplice mappa details.fields è facile da generare e comoda per i client da mostrare accanto ai campi.
Poi scrivi una mappa di status code e attieniti ad essa. Meno dibattito per endpoint, meglio è. Se vuoi sia 400 che 422, rendi esplicita la divisione:
bad_json -> 400 Bad Request (JSON malformato)validation_failed -> 422 Unprocessable Content (JSON ben formato, campi non validi)not_found -> 404 Not Foundconflict -> 409 Conflict (chiave duplicata, mismatch di versione)unauthorized -> 401 Unauthorizedforbidden -> 403 Forbiddeninternal -> 500 Internal Server ErrorDecidi cosa loggare vs cosa restituire. Una buona regola: i client ricevono un messaggio sicuro e un request ID; i log contengono l’errore completo e il contesto interno (SQL, payload upstream, user ID) che non vorresti mai esporre.
Infine, standardizza request_id. Accetta un header ID se presente (da un API gateway), altrimenti generane uno al bordo (middleware). Allegalo al context, includilo nei log e restituiscilo in ogni risposta di errore.
Se vuoi che lo scaffolding rimanga coerente, la struttura delle cartelle deve essere noiosa e ripetibile. I generatori seguono pattern che possono vedere, ma deragliano quando i file sono sparsi o i nomi cambiano per feature.
Scegli una convenzione di naming e non piegarla. Usa una parola fissa per ogni cosa: handler, service, repo, request, response. Se la route è POST /users, nomina file e tipi attorno a users e create (non usare a volte register, a volte addUser).
Un layout semplice che mappa i livelli abituali:
internal/
httpapi/
handlers/
users_handler.go
services/
users_service.go
data/
users_repo.go
apitypes/
users_types.go
Decidi dove vivono i tipi condivisi, perché qui i progetti si incasinano spesso. Una regola utile:
internal/apitypes (corrispondono al JSON e alle esigenze di validazione).Se un tipo ha tag JSON ed è progettato per i client, trattalo come un tipo API.
Mantieni le dipendenze degli handler minime e rendi la regola esplicita:
Scrivi un piccolo documento di pattern nella root del repo (Markdown semplice va bene). Includi l’albero delle cartelle, le regole di naming e un piccolo esempio di flow (handler -> service -> repo, più in quale file va ogni pezzo). Questa è la referenza esatta che incolli nel generatore così i nuovi endpoint corrispondono sempre alla struttura.
Prima di generare dieci endpoint, crea uno che consideri affidabile. Questo è il gold standard: il file che puoi indicare e dire: “Il nuovo codice deve assomigliare a questo.” Puoi scriverlo da zero o rifattorizzare uno esistente finché non combacia.
Mantieni il handler sottile. Una mossa che aiuta molto: inserire un’interfaccia tra handler e service così l’handler dipende da un contratto, non da una struct concreta.
Aggiungi piccoli commenti nell’endpoint di riferimento solo dove il codice generato potrebbe inciampare in futuro. Spiega decisioni (perché 400 vs 422, perché il create ritorna 201, perché nascondi errori interni dietro un messaggio generico). Evita commenti che semplicemente ripetono il codice.
Quando l’endpoint di riferimento funziona, estrai helper così ogni nuovo endpoint ha meno possibilità di deviare. Gli helper più riutilizzabili sono di solito:
Ecco come può apparire nella pratica il concetto di “handler sottile + interfaccia”:
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)
}
Conferma il pattern con un paio di test (anche un piccolo table test per la mappatura degli errori). La generazione funziona meglio quando ha un bersaglio pulito da imitare.
La coerenza inizia da cosa incolli e cosa proibisci. Per un nuovo endpoint, fornisci due cose:
Includi handler, metodo service, tipi request/response e qualsiasi helper condiviso che l’endpoint usa. Poi dichiara il contratto in termini chiari:
POST /v1/widgets)Sii esplicito su cosa deve corrispondere: naming, path dei package e funzioni helper (WriteJSON, BindJSON, WriteError, il tuo validator).
Un prompt rigido previene “refactor utili”. Per esempio:
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 usi test, richiedili esplicitamente (e nomina il file di test). Altrimenti il modello può saltarli o inventare un setup di test.
Esegui un rapido controllo diff dopo la generazione. Se ha modificato helper condivisi, la registrazione del router o il formato di errore standard, rigetta l’output e ribadisci le regole “non toccare” più severamente.
L’output è tanto coerente quanto l’input che fornisci. Il modo più veloce per evitare codice “quasi corretto” è riutilizzare un template di prompt ogni volta, con uno snapshot di contesto tratto dal tuo repo.
Copia, incolla e riempi i segnaposto:
You are editing an existing Go HTTP API.
CONTEXT
- Folder tree (only the relevant parts):
<pasta un piccolo albero: internal/http, internal/service, internal/repo, etc>
- Key types and patterns:
- Handler signature style: <esempio>
- Service interface style: <esempio>
- 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.
Questo funziona perché obbliga tre cose: un blocco contesto (cosa esiste), un blocco vincoli (cosa non fare) ed esempi JSON concreti (così le forme non scivolano). L’istruzione finale è il tuo freno di sicurezza: se il modello non è sicuro, dovrebbe dirlo prima di impegnarsi a cambiare il pattern.
Supponi di voler aggiungere un endpoint “Create project”. L’obiettivo è semplice: accettare un nome, applicare alcune regole, salvarlo e restituire un nuovo ID. La parte difficile è mantenere la stessa separazione handler-service-repo e lo stesso JSON di errore che già usi.
Un flow coerente appare così:
Ecco la request che l’handler accetta:
{ "name": "Roadmap", "owner_id": "u_123" }
In caso di successo, ritorna 201 Created. L’ID dovrebbe provenire sempre dalla stessa fonte. Per esempio, lascia che Postgres lo generi e fai restituire l’ID dal repo:
{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }
Due percorsi di fallimento realistici:
Se la validazione fallisce (nome mancante o troppo corto), restituisci un errore a livello di campo usando il tuo formato standard e lo status scelto:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }
Se il nome deve essere unico per owner e il service trova un progetto esistente, restituisci 409 Conflict:
{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }
Una decisione che mantiene il pattern pulito: l’handler controlla “questa richiesta ha la forma corretta?” mentre il service possiede “questo è permesso?”. Questa separazione rende gli endpoint generati prevedibili.
Il modo più rapido per perdere coerenza è lasciare che il generatore improvvisi.
Una deriva comune è un nuovo schema di errore. Un endpoint restituisce {error: "..."}, un altro restituisce {message: "..."}, un terzo aggiunge un oggetto nidificato. Risolvi mantenendo un solo envelope di errore e una sola mappa di status code in un posto, poi richiedi ai nuovi endpoint di riutilizzarli tramite import path e nome funzione. Se il generatore propone un nuovo campo, trattalo come una richiesta di modifica API, non come una comodità.
Un’altra deriva è il gonfiamento dell’handler. Inizia piccolo: valida, poi controlla permessi, poi query al DB, poi branching sulle regole di business. Presto ogni handler appare diverso. Mantieni una regola: gli handler traducono HTTP in input/output tipizzati; i service possiedono le decisioni; l’accesso ai dati possiede le query.
I mismatch di naming si sommano anche loro. Se un endpoint usa CreateUserRequest e un altro NewUserPayload, perderai tempo a cercare tipi e a scrivere adattatori. Scegli una convenzione di naming e rifiuta nuovi nomi salvo motivi forti.
Non restituire mai errori grezzi del database ai client. Oltre a esporre dettagli, crea messaggi incoerenti e status code diversi. Wrappa gli errori interni, logga la causa e restituisci un codice pubblico stabile.
Evita di aggiungere nuove librerie “solo per comodità”. Ogni validator, helper router o package per errori in più diventa un altro stile da uniformare.
Guardrail che prevengono la maggior parte dei guasti:
Se non riesci a confrontare due endpoint e vedere la stessa struttura (import, flow, gestione errori), stringi il prompt e rigenera prima del merge.
Prima di mergiare qualsiasi cosa generata, controlla prima la struttura. Se la forma è giusta, i bug di logica sono più facili da trovare.
Controlli di struttura:
request_id.Controlli di comportamento:
Tratta il tuo pattern come un contratto condiviso, non come una preferenza. Tieni il documento “come costruire endpoint” vicino al codice e mantieni un endpoint di riferimento che mostri l’approccio end-to-end.
Scala la generazione in piccoli batch. Genera 2–3 endpoint che coprano angoli diversi (una lettura semplice, una create con validazione, un update con caso not-found). Poi fermati e affina. Se le review continuano a trovare la stessa deriva di stile, aggiorna il documento baseline e l’endpoint di riferimento prima di generare altro.
Un loop ripetibile:
Se vuoi un ciclo build-review più serrato, una piattaforma vibe-coding come Koder.ai (koder.ai) può aiutare a scaffoldare e iterare velocemente in un workflow chat-driven, poi esportare il sorgente quando corrisponde al tuo standard. Lo strumento conta meno della regola: il tuo baseline rimane il referente.
Blocca un template ripetibile presto: una separazione coerente dei livelli (handler → service → data access), un unico envelope di errori e una mappa di status code. Poi usa un singolo “endpoint di riferimento” come esempio che ogni nuovo endpoint deve copiare.
Mantieni i handler limitati all’HTTP:
Se vedi SQL, controlli di permessi o branching di business nel handler, sposta quella logica nel service.
Metti regole di business e decisioni nel service:
Il service deve restituire risultati di dominio ed errori tipizzati—niente status HTTP, niente shaping JSON.
Isola le preoccupazioni di persistenza:
Evita di codificare i formati di risposta API o di imporre regole di business nel repo oltre alla integrità minima dei dati.
Una regola semplice di default:
Esempio: il handler verifica che email sia presente e abbia formato valido; il service verifica che sia consentita e non già in uso.
Usa un unico envelope di errore ovunque e mantienilo stabile. Una forma pratica è:
code per macchine (stabile)message per umani (breve e sicuro)details per dati strutturati (per esempio errori per campo)request_id per tracingQuesto evita casi speciali lato client e rende prevedibili gli endpoint generati.
Scrivi una mappa di status code e seguila sempre. Una suddivisione comune:
Restituisci errori pubblici sicuri e coerenti, e registra la causa reale internamente.
code stabile, message breve, più request_idQuesto evita leak interni e differenze casuali nei messaggi di errore tra endpoint.
Crea un endpoint “golden” che ritieni corretto e richiedi che i nuovi endpoint gli somiglino:
BindJSON, WriteJSON, WriteError, ecc.)Aggiungi un paio di test piccoli (anche table test per la mappatura degli errori) per fissare il pattern.
Dai al modello un contesto e vincoli rigidi:
Dopo la generazione, rifiuta diff che “migliorano” l’architettura invece di seguire il baseline.
400 per JSON malformato (bad_json)422 per errori di validazione (validation_failed)404 per risorse mancanti (not_found)409 per conflitti (duplicati/gestione versione)500 per errori inattesiLa chiave è la coerenza: niente discussioni endpoint per endpoint.