Claude Code für Go‑API‑Scaffolding: Definiere ein sauberes Handler‑Service‑Fehler‑Muster und generiere neue Endpunkte, die in deiner Go‑API konsistent bleiben.

Go‑APIs starten meist sauber: ein paar Endpunkte, ein oder zwei Personen, und alles lebt in den Köpfen. Dann wächst die API, Features werden unter Zeitdruck geliefert und kleine Unterschiede schleichen sich ein. Jeder für sich fühlt sich harmlos an, zusammen verlangsamen sie jede spätere Änderung.
Ein häufiges Beispiel: Ein Handler dekodiert JSON in ein Struct und gibt 400 mit einer hilfreichen Nachricht zurück, ein anderer gibt 422 mit anderer Form zurück, und ein dritter loggt Fehler in anderem Format. Nichts davon bricht die Kompilierung. Es erzeugt aber dauernde Entscheidungsfindung und kleine Umschreibungen bei jedem neuen Endpoint.
Den Schlamassel spürst du an Stellen wie:
CreateUser, AddUser, RegisterUser), die die Suche erschwert.„Scaffolding“ bedeutet hier eine wiederholbare Vorlage für neue Arbeit: wohin Code gehört, was jede Schicht macht und wie Antworten aussehen. Es geht weniger ums massenhafte Generieren von Code als darum, eine konsistente Form festzusetzen.
Tools wie Claude können helfen, neue Endpunkte schnell zu generieren, aber sie bleiben nur nützlich, wenn du das Muster als Regel behandelst. Du definierst die Regeln, reviewst jeden Diff und fährst Tests. Das Modell füllt die Standardteile aus; es darf deine Architektur nicht neu definieren.
Eine Go‑API bleibt leicht zu erweitern, wenn jede Anfrage denselben Pfad folgt. Bevor du Endpunkte generierst, wähle eine Schichtaufteilung und halte dich daran.
Die Aufgabe des Handlers ist ausschließlich HTTP: Anfrage lesen, Service aufrufen und Antwort schreiben. Er sollte keine Business‑Regeln, kein SQL und kein „nur dieser eine Spezialfall“ enthalten.
Der Service verantwortet den Use Case: Geschäftsregeln, Entscheidungen und Orchestrierung über Repositories oder externe Aufrufe. Er sollte nichts über HTTP‑Belange wissen wie Statuscodes, Header oder wie Fehler gerendert werden.
Data Access (Repository/Store) kümmert sich um Persistenzdetails. Er übersetzt Service‑Intention in SQL/Queries/Transaktionen. Er darf Geschäftsregeln nur soweit durchsetzen, wie es die Datenintegrität verlangt, und soll keine API‑Antworten formen.
Eine praktische Trennungsliste:
Wähle eine Regel und biege sie nicht.
Ein einfacher Ansatz:
Beispiel: Der Handler prüft, dass email vorhanden ist und wie eine E‑Mail aussieht. Der Service prüft, dass die E‑Mail erlaubt ist und noch nicht verwendet wird.
Entscheide früh, ob Services Domain‑Typen oder DTOs zurückgeben.
Eine saubere Voreinstellung ist: Handler benutzen Request/Response‑DTOs, Services Domain‑Typen, und der Handler mapped Domain → Response. Das hält den Service stabil, auch wenn sich der HTTP‑Vertrag ändert.
Wenn das Mapping sich schwerfällig anfühlt, halte trotzdem die Konsistenz: Der Service gibt einen Domain‑Typ plus einen typisierten Fehler zurück, und das JSON‑Shaping bleibt im Handler.
Wenn generierte Endpunkte so aussehen sollen, als wären sie von derselben Person geschrieben worden, sperre Fehler‑Antworten früh ein. Generierung funktioniert am besten, wenn das Ausgabeformat nicht verhandelbar ist: eine JSON‑Form, eine Statuscode‑Map und eine Regel, was offengelegt wird.
Beginne mit einer einzigen Fehler‑Hülle, die jeder Endpunkt bei Fehlern zurückgibt. Halte sie klein und vorhersehbar:
{
"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..."
}
Verwende code für Maschinen (stabil und vorhersehbar) und message für Menschen (kurz und sicher). Packe optionale strukturierte Daten in details. Für Validierung ist eine einfache details.fields‑Map leicht zu generieren und einfach für Clients, neben Eingaben angezeigt zu werden.
Schreibe dann eine Statuscode‑Map und halte dich daran. Je weniger Diskussion pro Endpoint, desto besser. Wenn du sowohl 400 als auch 422 willst, mache die Trennung explizit:
bad_json -> 400 Bad Request (malformed JSON)validation_failed -> 422 Unprocessable Content (wohlgeformtes JSON, ungültige Felder)not_found -> 404 Not Foundconflict -> 409 Conflict (duplizierter Schlüssel, Versionskonflikt)unauthorized -> 401 Unauthorizedforbidden -> 403 Forbiddeninternal -> 500 Internal Server ErrorEntscheide, was du loggst vs. was du zurückgibst. Eine gute Regel: Clients bekommen eine sichere Nachricht und eine request_id; Logs enthalten den vollständigen Fehler und internen Kontext (SQL, Upstream‑Payloads, User‑IDs), den du niemals leaken würdest.
Standardisiere schließlich request_id. Akzeptiere eine eingehende ID‑Header, falls vorhanden (z. B. vom API‑Gateway), andernfalls generiere eine am Edge (Middleware). Hänge sie an den Context, füge sie den Logs bei und gib sie in jeder Fehlerantwort zurück.
Wenn Scaffolding konsistent bleiben soll, muss deine Ordnerstruktur langweilig und wiederholbar sein. Generatoren folgen Mustern, die sie erkennen können; sie driftet, wenn Dateien verstreut sind oder Namen pro Feature variieren.
Wähle eine Namenskonvention und halte dich daran. Verwende ein Wort für jede Sache: handler, service, repo, request, response. Wenn die Route POST /users heißt, benenne Dateien und Typen rund um users und create (nicht mal register, mal addUser).
Ein einfaches Layout, das die üblichen Schichten widerspiegelt:
internal/
httpapi/
handlers/
users_handler.go
services/
users_service.go
data/
users_repo.go
apitypes/
users_types.go
Entscheide, wo Shared‑Typen leben, denn hier wird es oft unordentlich. Eine nützliche Regel:
internal/apitypes (passen zu JSON und Validierung)Wenn ein Typ JSON‑Tags hat und für Clients gedacht ist, behandle ihn als API‑Typ.
Halte Handler‑Dependencies minimal und mache die Regel explizit:
Schreibe ein kurzes Pattern‑Dokument in die Repo‑Root (Plain Markdown reicht). Füge den Ordnerbaum, Namensregeln und einen kleinen Beispiel‑Flow bei (handler -> service -> repo, plus in welcher Datei jedes Stück liegt). Das ist die Referenz, die du in deinen Generator kopierst, damit neue Endpunkte jedes Mal dieselbe Struktur haben.
Bevor du zehn Endpunkte generierst, erstelle einen Endpoint, dem du vertraust. Das ist der Goldstandard: die Datei, auf die du zeigen kannst und sagst: „Neuer Code muss so aussehen.“ Du kannst ihn neu schreiben oder einen bestehenden refaktorieren, bis er passt.
Halte den Handler dünn. Eine kleine Maßnahme, die hilft: lege ein Interface zwischen Handler und Service, sodass der Handler von einem Vertrag abhängt, nicht von einer konkreten Implementierung.
Füge nur kurze Kommentare im Referenz‑Endpoint dort hinzu, wo generierter Code stolpern könnte. Erkläre Entscheidungen (warum 400 vs 422, warum create 201 zurückgibt, warum interne Fehler hinter einer generischen Nachricht verborgen werden). Überspringe Kommentare, die nur den Code wiederholen.
Wenn der Referenz‑Endpoint funktioniert, extrahiere Helfer, sodass jeder neue Endpoint weniger Chancen zum Driften hat. Die wiederverwendbarsten Helfer sind meist:
So kann ein „dünner Handler + Interface“ in der Praxis aussehen:
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)
}
Verankere das mit ein paar Tests (selbst ein kleines Table‑Test für Error‑Mapping reicht). Generierung funktioniert am besten, wenn sie ein sauberes Ziel hat, das sie imitieren soll.
Konsistenz beginnt mit dem, was du einfügst, und dem, was du verbietest. Für einen neuen Endpoint gib zwei Dinge:
Füge Handler, Service‑Methode, Request/Response‑Typen und alle Shared‑Helfer bei, die der Endpoint nutzt. Dann formuliere den Vertrag in klaren Worten:
POST /v1/widgets)Sei explizit, was übereinstimmen muss: Namensgebung, Paketpfade und Helfer (WriteJSON, BindJSON, WriteError, dein Validator).
Ein strikter Prompt verhindert „hilfreiche“ Refactorings. Zum Beispiel:
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.
Wenn du Tests möchtest, fordere sie explizit an (und benenne die Testdatei). Ansonsten könnte das Modell sie überspringen oder ein Test‑Setup erfinden.
Mache nach der Generierung einen schnellen Diff‑Check. Wenn gemeinsame Helfer, Router‑Registrierung oder dein Standard‑Error‑Format verändert wurden, lehne die Ausgabe ab und setze die „do not change“‑Regeln strenger.
Die Ausgabe ist nur so konsistent wie die Eingabe. Der schnellste Weg, „fast fast richtig“ Code zu vermeiden, ist, dieselbe Prompt‑Vorlage immer wieder zu verwenden und ein kleines Kontext‑Snapshot aus deinem Repo beizufügen.
Kopiere, fülle die Platzhalter und verwende sie:
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.
Das wirkt, weil es drei Dinge erzwingt: einen Kontext‑Block (was existiert), einen Constraints‑Block (was nicht zu tun ist) und konkrete JSON‑Beispiele (damit Shapes nicht driften). Die letzte Anweisung ist dein Sicherheitsnetz: Wenn das Modell unsicher ist, soll es vor dem Schreiben fragen.
Angenommen, du willst einen „Create project“ Endpoint hinzufügen. Ziel: einen Namen akzeptieren, ein paar Regeln durchsetzen, speichern und eine neue ID zurückgeben. Knifflig ist, denselben Handler‑Service‑Repo‑Split und dieselbe Error‑JSON beizubehalten.
Ein konsistenter Ablauf sieht so aus:
Hier die Anfrage, die der Handler akzeptiert:
{ "name": "Roadmap", "owner_id": "u_123" }
Bei Erfolg: 201 Created zurückgeben. Die ID sollte immer aus derselben Quelle kommen. Zum Beispiel Postgres sie generieren lassen und vom Repo zurückgeben:
{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }
Zwei realistische Fehlerpfade:
Wenn die Validierung fehlschlägt (fehlender oder zu kurzer Name), gib einen Feldfehler in deiner Standardform und mit dem gewählten Statuscode zurück:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }
Wenn der Name pro Owner eindeutig sein muss und der Service ein bestehendes Projekt findet, gib 409 Conflict zurück:
{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }
Eine Entscheidung, die das Muster sauber hält: Der Handler prüft „ist die Anfrage korrekt geformt?“, während der Service prüft „ist das erlaubt?“. Diese Trennung macht generierte Endpunkte vorhersehbar.
Der schnellste Weg, Konsistenz zu verlieren, ist, dem Generator zu erlauben, zu improvisieren.
Ein häufiger Drift ist eine neue Fehler‑Form. Ein Endpoint gibt {error: "..."} zurück, ein anderer {message: "..."}, ein dritter fügt ein verschachteltes Objekt hinzu. Behebe das, indem du eine einzige Fehler‑Hülle und eine Statuscode‑Map an einem Ort hältst und neue Endpunkte zwingst, diese per Importpfad und Funktionsnamen wiederzuverwenden. Wenn der Generator ein neues Feld vorschlägt, behandle das wie eine API‑Änderung, nicht als Bequemlichkeit.
Handler‑Bloat ist ein weiterer Drift. Es startet klein: validieren, dann Permissions prüfen, dann DB abfragen, dann auf Business‑Regeln verzweigen. Bald sieht jeder Handler anders aus. Eine Regel: Handler übersetzen HTTP in typisierte Inputs/Outputs; Services übernehmen Entscheidungen; Data Access übernimmt Queries.
Namensmismatch addiert sich ebenfalls. Wenn ein Endpoint CreateUserRequest verwendet und ein anderer NewUserPayload, verschwendest du Zeit beim Suchen und Schreibkonnektoren. Wähle ein Namensschema und lehne neue Namen ab, außer es gibt einen guten Grund.
Gib niemals rohe Datenbankfehler an Clients zurück. Abgesehen davon, dass du Details leakst, erzeugt es inkonsistente Nachrichten und Statuscodes. Wrappe interne Fehler, logge die Ursache und gib einen stabilen öffentlichen Fehlercode zurück.
Vermeide es, neue Bibliotheken „nur aus Bequemlichkeit“ hinzuzufügen. Jeder zusätzliche Validator, Router‑Helper oder Error‑Paket wird zu einem weiteren Stil, den es abzustimmen gilt.
Guardrails, die die meisten Probleme verhindern:
Wenn du zwei Endpunkte nicht diffen kannst und dieselbe Form (Imports, Flow, Error‑Handling) erkennst, verschärfe den Prompt und regeneriere, bevor du merge.
Bevor du etwas generiertes mergest, prüfe zuerst die Struktur. Wenn die Form stimmt, sind Logikbugs leichter zu finden.
Structure‑Checks:
request_id‑Verhalten.Behavior‑Checks:
not found oder conflict) auslösen und Status/JSON‑Shape prüfen.Behandle dein Muster als gemeinsamen Vertrag, nicht als Vorliebe. Halte das „wie wir Endpunkte bauen“‑Dokument neben dem Code und pflege einen Referenz‑Endpoint, der den kompletten Ansatz end‑to‑end zeigt.
Skaliere die Generierung in kleinen Chargen. Generiere 2–3 Endpunkte, die verschiedene Ecken abdecken (ein einfacher Read, ein Create mit Validierung, ein Update mit Not‑Found‑Fall). Dann stoppe und verfeinere. Wenn Reviews immer dieselben Stil‑Drifts finden, aktualisiere das Baseline‑Dok und den Referenz‑Endpoint bevor du mehr generierst.
Eine wiederholbare Schleife:
Wenn du einen strengeren Build‑Review‑Loop willst, kann eine Vibe‑Coding‑Plattform wie Koder.ai (koder.ai) helfen, Scaffoldings und Iterationen schnell in einem Chat‑getriebenen Workflow zu machen und den Source zu exportieren, sobald er deinem Standard entspricht. Das Tool ist weniger entscheidend als die Regel: Die Baseline bleibt das Kommando.
Locke ein wiederholbares Template früh fest: eine konsistente Schichtung (Handler → Service → Data Access), eine einheitliche Fehler-Hülle und eine Zuordnung von Statuscodes. Verwende dann einen einzelnen „Reference Endpoint“ als Beispiel, dem jeder neue Endpoint folgen muss.
Halte Handler HTTP-only:
Wenn du SQL, Berechtigungsprüfungen oder Business-Logik in einem Handler siehst, verschiebe das in den Service.
Platziere Geschäftsregeln und Entscheidungen im Service:
Der Service sollte Domain-Ergebnisse und typisierte Fehler zurückgeben—keine HTTP-Statuscodes, kein JSON-Shaping.
Isoliere Persistenz‑Belange:
Vermeide, API-Antwortformate zu kodieren oder Business‑Regeln im Repo durchzusetzen (außer grundlegende Datenintegrität).
Eine einfache Standard‑Aufteilung:
Beispiel: Der Handler prüft, dass email vorhanden ist und wie eine E‑Mail aussieht; der Service prüft, ob sie erlaubt ist und noch nicht verwendet wird.
Verwende überall dieselbe Fehler‑Hülle und halte sie stabil. Eine praktikable Form ist:
code für Maschinen (stabil)message für Menschen (kurz und sicher)details für strukturierte Extras (wie Feldfehler)request_id zur NachverfolgungDas vermeidet spezielle Fälle auf Client‑Seite und macht generierte Endpunkte vorhersehbar.
Schreibe eine Statuscode‑Map fest und halte dich daran. Eine übliche Aufteilung:
Gebe sichere, konsistente öffentliche Fehler zurück und logge die echte Ursache intern.
code, kurze message, plus request_idSo vermeidest du Informationsleaks und inkonsistente Fehlermeldungen über Endpunkte hinweg.
Erstelle einen „golden“ Reference Endpoint, dem neue Endpunkte entsprechen müssen:
BindJSON, WriteJSON, WriteError usw.)Füge ein paar kleine Tests hinzu (z. B. Table‑Tests für Error‑Mapping), um das Muster zu verankern.
Gib dem Modell strengen Kontext und klare Einschränkungen:
Nach der Generierung: lehne Diffs ab, die die Architektur „verbessern“ statt dem Baseline‑Muster zu folgen.
400 für fehlerhaftes JSON (bad_json)422 für Validierungsfehler (validation_failed)404 für nicht gefundene Ressourcen (not_found)409 für Konflikte (Duplikate/Version‑Mismatch)500 für unerwartete FehlerWichtig ist Konsistenz: keine Einzelfall‑Debatten pro Endpoint.