6 min

Claude Code per lo scaffolding delle API Go: handler e servizi coerenti

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.

Claude Code per lo scaffolding delle API Go: handler e servizi coerenti

Perché le API Go diventano disordinate quando i pattern non sono fissati presto

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:

  • Corpi di errore diversi tra endpoint, quindi i client devono gestire casi speciali.
  • Service che a volte restituiscono errori grezzi del database e altre volte errori “amichevoli”.
  • Deriva di naming (CreateUser, AddUser, RegisterUser) che rende le ricerche più difficili.
  • Validazioni che si spostano, così gli stessi bug tornano a ripetersi.

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.

Scegli una divisione dei livelli: handler, service e accesso ai dati

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.

Responsabilità, mantenute semplici

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:

  • Handler: parse input, chiamare il service, mappare errori in HTTP, scrivere JSON
  • Service: applicare regole di business, chiamare i repo, restituire risultati di dominio o errori tipati
  • Data access: eseguire query, mappare righe in struct, restituire errori di storage
  • Condiviso: tipi di errore comuni e helper di risposta
  • Nessun livello salta un altro (l’handler non chiama mai direttamente il repo)

Una regola per la validazione

Scegli una regola e non piegarla.

Un approccio semplice:

  • Gli handler fanno la “validazione di forma” (campi richiesti, formato base).
  • I service fanno la “validazione di significato” (permessi, invarianti, stato).

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.

Cosa si sposta tra i livelli

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.

Definisci una risposta di errore standard e una mappa di status code

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 Found
  • conflict -> 409 Conflict (chiave duplicata, mismatch di versione)
  • unauthorized -> 401 Unauthorized
  • forbidden -> 403 Forbidden
  • internal -> 500 Internal Server Error

Decidi 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.

Layout delle cartelle e naming che rendono la generazione prevedibile

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:

  • I tipi request/response API vivono in internal/apitypes (corrispondono al JSON e alle esigenze di validazione).
  • I tipi di dominio vivono più vicino al layer del service (corrispondono alle regole di business).

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:

  • Handlers importano solo: routing/http, context, apitypes e services
  • Services importano: tipi di dominio e accesso ai dati
  • Data access importa: driver DB e helper per query
  • Nessun handler importa direttamente il pacchetto del database

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.

Crea un endpoint di riferimento che definisca il pattern

Possiedi il codice che invii
Genera, revisiona, poi esporta il sorgente quando la struttura corrisponde alle regole del repo.

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:

  • Bind JSON e gestire corpi malformati
  • Validare input e restituire errori per campo
  • Scrivere risposte JSON in modo consistente
  • Mappare errori di dominio in status HTTP

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.

Passo dopo passo: fai generare a Claude un nuovo endpoint che corrisponda

La coerenza inizia da cosa incolli e cosa proibisci. Per un nuovo endpoint, fornisci due cose:

  1. Il tuo endpoint di riferimento (l’esempio “perfetto”)
  2. Una breve nota di pattern che nomina package, funzioni e helper che deve seguire

1) Fornisci prima il contesto (riferimento + regole)

Includi handler, metodo service, tipi request/response e qualsiasi helper condiviso che l’endpoint usa. Poi dichiara il contratto in termini chiari:

  • Route + metodo (esempio: POST /v1/widgets)
  • Campi JSON della request (obbligatori vs opzionali)
  • Forma del JSON di risposta
  • Casi d’errore e status code
  • File che ti aspetti indietro (e solo quelli)

Sii esplicito su cosa deve corrispondere: naming, path dei package e funzioni helper (WriteJSON, BindJSON, WriteError, il tuo validator).

2) Chiedi l’output nella forma esatta che vuoi

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.

Un template di prompt riutilizzabile per scaffold coerenti

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.

Template di prompt

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.

Esempio realistico: aggiungere un endpoint Create senza rompere lo stile

Consolida un unico pattern API
Trasforma il tuo endpoint di riferimento in template ripetibili per ogni nuova route.

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ì:

  • Handler: bind JSON, fare validazione base dei campi, chiamare il service
  • Service: applicare regole di business (come unicità), chiamare il repo, restituire un risultato di dominio
  • Repo: scrivere su Postgres, restituire l’ID generato

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.

Errori comuni che rompono la consistenza (e come evitarli)

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:

  • Richiedere ai nuovi endpoint di riutilizzare tipi di errore e helper esistenti.
  • Tenere gli handler privi di regole di business e accesso DB.
  • Far rispettare una convenzione di naming per request/response.
  • Mappare errori interni in codici pubblici, mai errori grezzi.
  • Aggiungere dipendenze solo con una ragione scritta e chiara.

Se non riesci a confrontare due endpoint e vedere la stessa struttura (import, flow, gestione errori), stringi il prompt e rigenera prima del merge.

Checklist rapida prima di fare il merge di un endpoint generato

Pianifica prima i livelli
Mappa handler, servizi e repo prima di generare codice per ridurre la deriva.

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:

  • Il flow dell’handler è coerente: bind input, validate, call service, mappa errori di dominio in HTTP, scrivi la response.
  • Il codice del service contiene solo regole di business: niente HTTP o JSON.
  • Le risposte di successo corrispondono allo stile di casa (o envelope condiviso ovunque o JSON diretto ovunque).
  • Le risposte di errore sono uniformi: stesse campi JSON, stessi codici, stesso comportamento di request_id.
  • Naming e collocazione sono noiosi: nomi file, funzioni e route corrispondono agli endpoint esistenti e tutto è formattato con gofmt.

Controlli di comportamento:

  • Esegui i test e aggiungi almeno un test piccolo per un nuovo branch di handler/service.
  • Conferma che i fallimenti di validazione restituiscano lo stesso codice e status di endpoint simili.
  • Triggera un errore di service noto (tipo “not found” o “conflict”) e conferma status HTTP e forma JSON.
  • Cerca residui di copia-incolla: route sbagliata, messaggi di log errati, nomi DTO non corrispondenti.
  • Compila e avvia il server localmente almeno una volta per verificare wiring e import.

Prossimi passi: standardizza il pattern, poi scala la generazione in sicurezza

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:

  • Scrivi il baseline: nomi file, nomi funzioni, struct request/response, codici di errore e dove avviene la validazione.
  • Conserva un endpoint “golden” e aggiornalo per primo ogni volta che il pattern cambia.
  • Genera in batch pochi endpoint, revisiona per coerenza, poi aggiorna prompt e doc di pattern.
  • Rifattorizza gli endpoint più vecchi a blocchi e tieni una strada di rollback se il comportamento cambia.
  • Monitora una metrica per una settimana: tempo per aggiungere un endpoint, tasso di bug dopo il merge o tempo medio in review.

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.

Domande frequenti

What’s the fastest way to stop a Go API from getting inconsistent?

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.

What should a handler do (and not do)?

Mantieni i handler limitati all’HTTP:

  • Bind/parsare la richiesta
  • Fare la validazione di “shape” (campi obbligatori, formati semplici)
  • Chiamare il service
  • Mappare errori tipizzati in status code HTTP
  • Scrivere JSON usando helper condivisi

Se vedi SQL, controlli di permessi o branching di business nel handler, sposta quella logica nel service.

What belongs in the service layer?

Metti regole di business e decisioni nel service:

  • Permessi e regole di accesso
  • Invarianti (transizioni di stato, regole di unicità, controlli di “consentito”)
  • Orchestrazione tra repository e chiamate esterne

Il service deve restituire risultati di dominio ed errori tipizzati—niente status HTTP, niente shaping JSON.

What belongs in the data access/repository layer?

Isola le preoccupazioni di persistenza:

  • SQL/query e transazioni
  • Mapping delle righe in struct
  • Restituire errori di storage (che il service può interpretare)

Evita di codificare i formati di risposta API o di imporre regole di business nel repo oltre alla integrità minima dei dati.

Where should validation live?

Una regola semplice di default:

  • I handler validano la forma della richiesta (campi mancanti, formati di base)
  • I service validano il significato (permessi, invarianti, stato)

Esempio: il handler verifica che email sia presente e abbia formato valido; il service verifica che sia consentita e non già in uso.

What should a standard API error response look like?

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 tracing

Questo evita casi speciali lato client e rende prevedibili gli endpoint generati.

How do I choose between 400 vs 422 vs 409 for errors?

Scrivi una mappa di status code e seguila sempre. Una suddivisione comune:

  • 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 inattesi

La chiave è la coerenza: niente discussioni endpoint per endpoint.

Should I ever return raw database errors to clients?

Restituisci errori pubblici sicuri e coerenti, e registra la causa reale internamente.

  • Risposta: code stabile, message breve, più request_id
  • Log: dettaglio completo dell’errore (SQL, payload upstream, user ID)

Questo evita leak interni e differenze casuali nei messaggi di errore tra endpoint.

What is a reference endpoint, and why do I need one?

Crea un endpoint “golden” che ritieni corretto e richiedi che i nuovi endpoint gli somiglino:

  • Stesso flusso (bind → validate → service → error map → JSON)
  • Stessi helper (BindJSON, WriteJSON, WriteError, ecc.)
  • Stessa struttura di cartelle e naming

Aggiungi un paio di test piccoli (anche table test per la mappatura degli errori) per fissare il pattern.

How do I prompt Claude to generate new endpoints without breaking my structure?

Dai al modello un contesto e vincoli rigidi:

  • Incolla l’endpoint di riferimento e le regole di pattern
  • Specifica route, esempi JSON di request/response e i casi d’errore
  • Elenca esattamente quali file può produrre
  • Dì esplicitamente cosa non deve toccare (router, formato errori, helper)

Dopo la generazione, rifiuta diff che “migliorano” l’architettura invece di seguire il baseline.

Related posts