Claude Code pour le scaffolding d'API Go : définissez un pattern handler-service-erreur clair, puis générez des endpoints qui restent cohérents dans toute votre API Go.

Les API Go commencent généralement propres : quelques endpoints, une ou deux personnes, et tout vit dans la tête de chacun. Puis l'API grandit, les fonctionnalités sont livrées sous pression, et de petites différences s'insinuent. Chacune semble inoffensive, mais ensemble elles ralentissent chaque changement futur.
Un exemple fréquent : un handler décode du JSON dans une struct et renvoie 400 avec un message utile, un autre renvoie 422 avec une forme différente, et un troisième journalise les erreurs dans un format différent. Rien de tout cela ne casse la compilation. Ça crée juste des décisions constantes et de petites réécritures à chaque ajout.
Vous sentez le désordre dans des endroits comme :
CreateUser, AddUser, RegisterUser) qui complique la recherche.Par « scaffolding » ici, on entend un modèle répétable pour le nouveau travail : où le code va, ce que fait chaque couche, et à quoi ressemblent les réponses. Il s'agit moins de générer beaucoup de code que de verrouiller une forme cohérente.
Des outils comme Claude peuvent vous aider à scaffolder rapidement de nouveaux endpoints, mais ils restent utiles seulement si vous traitez le pattern comme une règle. Vous définissez les règles, vous révisez chaque diff, et vous exécutez des tests. Le modèle complète les parties standard ; il n'a pas le droit de redéfinir votre architecture.
Une API Go reste facile à faire évoluer quand chaque requête suit le même chemin. Avant de commencer à générer des endpoints, choisissez une séparation de couches et tenez-vous-y.
Le job du handler est uniquement HTTP : lire la requête, appeler le service et écrire la réponse. Il ne doit pas contenir des règles métier, du SQL, ou de la logique « juste ce cas spécial ».
Le service possède le cas d'utilisation : règles métier, décisions et orchestration entre repositories ou appels externes. Il ne doit pas connaître les préoccupations HTTP comme les codes de statut, les en-têtes ou la manière dont les erreurs sont rendues.
L'accès aux données (repository/store) possède les détails de persistance. Il traduit l'intention du service en SQL/queries/transactions. Il ne doit pas appliquer des règles métier au-delà de l'intégrité des données et ne doit pas façonner les réponses API.
Une checklist de séparation pratique :
Choisissez une règle et ne la déformez pas.
Une approche simple :
Exemple : le handler vérifie que email est présent et ressemble à un email. Le service vérifie que l'email est autorisé et n'est pas déjà utilisé.
Décidez tôt si les services retournent des types domaine ou des DTOs.
Un défaut propre : les handlers utilisent des DTOs request/response, les services utilisent des types domaine, et le handler mappe le domaine vers la réponse. Cela garde le service stable même si le contrat HTTP change.
Si le mapping paraît lourd, gardez la cohérence : faites en sorte que le service retourne un type domaine plus une erreur typée, et laissez le shaping JSON au handler.
Si vous voulez que les endpoints générés semblent écrits par la même personne, verrouillez les réponses d'erreur tôt. La génération est la plus efficace quand le format de sortie est non négociable : une seule forme JSON, une seule table de codes, et une règle sur ce qui est exposé.
Commencez par une unique enveloppe d'erreur que chaque endpoint renvoie en cas d'échec. Gardez-la petite et prévisible :
{
"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..."
}
Utilisez code pour les machines (stable et prévisible) et message pour les humains (court et sûr). Mettez les données structurées optionnelles dans details. Pour la validation, une simple map details.fields est facile à générer et simple pour les clients à afficher à côté des champs.
Ensuite, rédigez une table de correspondance des codes HTTP et tenez-vous-y. Moins il y a de débat par endpoint, mieux c'est. Si vous voulez à la fois 400 et 422, explicitez la séparation :
bad_json -> 400 Bad Request (JSON malformé)validation_failed -> 422 Unprocessable Content (JSON bien formé, champs invalides)not_found -> 404 Not Foundconflict -> 409 Conflict (clé dupliquée, mismatch de version)unauthorized -> 401 Unauthorizedforbidden -> 403 Forbiddeninternal -> 500 Internal Server ErrorDécidez ce que vous loggez vs ce que vous retournez. Une bonne règle : les clients reçoivent un message sûr et un request ID ; les logs contiennent l'erreur complète et le contexte interne (SQL, payloads upstream, IDs utilisateur) que vous ne voudriez jamais divulguer.
Enfin, standardisez request_id. Acceptez un header ID entrant si présent (depuis une gateway), sinon générez-en un au bord (middleware). Attachez-le au context, incluez-le dans les logs et retournez-le dans chaque réponse d'erreur.
Si vous voulez que le scaffolding reste cohérent, la structure de dossiers doit être ennuyeuse et répétable. Les générateurs suivent des motifs qu'ils peuvent reconnaître, mais ils dévient quand les fichiers sont éparpillés ou que les noms changent selon la feature.
Choisissez une convention de nommage et ne la déformez pas. Choisissez un mot pour chaque chose et tenez-le : handler, service, repo, request, response. Si la route est POST /users, nommez les fichiers et types autour de users et create (pas parfois register, parfois addUser).
Une disposition simple qui correspond aux couches habituelles :
internal/
httpapi/
handlers/
users_handler.go
services/
users_service.go
data/
users_repo.go
apitypes/
users_types.go
Décidez où vivent les types partagés, car c'est souvent là que les projets deviennent en désordre. Une règle utile :
internal/apitypes (correspondent au JSON et aux besoins de validation).Si un type contient des tags JSON et est conçu pour les clients, traitez-le comme un type API.
Gardez les dépendances des handlers minimales et explicitez cette règle :
Rédigez un petit document de pattern à la racine du repo (un Markdown simple suffit). Incluez l'arbre de dossiers, les règles de nommage et un petit flux d'exemple (handler -> service -> repo, plus quel fichier contient chaque morceau). C'est la référence exacte que vous collez dans votre générateur pour que les nouveaux endpoints respectent la structure à chaque fois.
Avant de générer dix endpoints, créez un endpoint que vous approuvez. C'est la référence : le fichier que vous pouvez pointer et dire « Le nouveau code doit ressembler à ça. » Vous pouvez l'écrire depuis zéro ou refactoriser un existant jusqu'à ce qu'il corresponde.
Gardez le handler mince. Un mouvement qui aide beaucoup : placez une interface entre le handler et le service pour que le handler dépende d'un contrat, pas d'une implémentation concrète.
Ajoutez de petits commentaires dans l'endpoint de référence uniquement là où le code généré pourrait buter. Expliquez des décisions (pourquoi 400 vs 422, pourquoi create renvoie 201, pourquoi vous cachez les erreurs internes derrière un message générique). Évitez les commentaires qui ne font que répéter le code.
Une fois l'endpoint de référence correct, extrayez des helpers pour réduire les chances de dérive sur chaque nouveau endpoint. Les helpers les plus réutilisables sont souvent :
Voici à quoi peut ressembler un « handler mince + interface » en pratique :
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)
}
Verrouillez-le avec quelques tests (même un petit table test pour le mapping d'erreurs). La génération est plus fiable quand elle a une cible propre à imiter.
La cohérence commence par ce que vous collez et ce que vous interdisez. Pour un nouvel endpoint, fournissez deux choses :
Incluez le handler, la méthode service, les types request/response et tout helper partagé utilisé par l'endpoint. Puis énoncez le contrat en termes simples :
POST /v1/widgets)Soyez explicite sur ce qui doit correspondre : nommage, chemins de package et fonctions helper (WriteJSON, BindJSON, WriteError, votre validator).
Une invite serrée empêche les refactorings « utiles ». Par exemple :
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.
Si vous utilisez des tests, demandez-les explicitement (et nommez le fichier de test). Sinon le modèle peut les sauter ou inventer une configuration de test.
Faites un rapide contrôle de diff après génération. Si des helpers partagés, l'enregistrement du router, ou votre réponse d'erreur standard ont été modifiés, rejetez la sortie et réitérez les règles « ne pas changer » plus strictement.
La sortie n'est cohérente que si l'entrée l'est. Le moyen le plus rapide d'éviter du code « presque correct » est de réutiliser une invite unique à chaque fois, avec un petit snapshot de contexte tiré de votre repo.
Collez, remplissez et adaptez les 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.
Ceci fonctionne car cela force trois choses : un bloc de contexte (ce qui existe), un bloc de contraintes (ce qu'il ne faut pas faire), et des exemples JSON concrets (pour que les formes ne dérivent pas). L'instruction finale est votre filet de sécurité : si le modèle est incertain, il doit demander avant d'engager des changements.
Supposons que vous vouliez ajouter un endpoint « Create project ». Le but est simple : accepter un nom, appliquer quelques règles, le stocker et retourner un nouvel ID. La difficulté est de conserver la séparation handler-service-repo et la même forme d'erreur que vous utilisez déjà.
Un flux cohérent ressemble à ceci :
Voici la requête que le handler accepte :
{ "name": "Roadmap", "owner_id": "u_123" }
En cas de succès, retourner 201 Created. L'ID doit provenir d'un seul endroit à chaque fois. Par exemple, laissez Postgres le générer et faites en sorte que le repo le retourne :
{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }
Deux chemins d'échec réalistes :
Si la validation échoue (nom manquant ou trop court), retournez une erreur au niveau des champs en utilisant votre forme standard et le code choisi :
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }
Si le nom doit être unique par owner et que le service trouve déjà un projet, retournez 409 Conflict :
{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }
Une décision qui garde le pattern propre : le handler vérifie « est-ce que la requête a la bonne forme ? » tandis que le service possède « est-ce permis ? ». Cette séparation rend les endpoints générés prévisibles.
Le moyen le plus rapide de perdre la cohérence est de laisser le générateur improviser.
Une dérive fréquente est une nouvelle forme d'erreur. Un endpoint renvoie {error: "..."}, un autre {message: "..."}, un troisième ajoute un objet imbriqué. Corrigez cela en gardant une seule enveloppe d'erreur et une table de codes en un seul endroit, puis exigez que les nouveaux endpoints les réutilisent via import path et nom de fonction. Si le générateur propose un nouveau champ, traitez-le comme une demande de changement d'API, pas comme une commodité.
Une autre dérive est l'alourdissement des handlers. Ça commence petit : valider, puis vérifier les permissions, puis requêter la DB, puis brancher sur des règles métier. Bientôt chaque handler est différent. Gardez une règle : les handlers traduisent HTTP en inputs/outputs typés ; les services possèdent les décisions ; l'accès aux données possède les requêtes.
Les mismatches de nommage s'accumulent aussi. Si un endpoint utilise CreateUserRequest et un autre NewUserPayload, vous perdrez du temps à chasser les types et écrire des adaptateurs. Choisissez un schéma de nommage et rejetez les nouveaux noms sauf raison forte.
Ne renvoyez jamais d'erreurs brutes de la base aux clients. En plus de divulguer des détails, cela crée des messages et des codes incohérents. Enveloppez les erreurs internes, loggez la cause et retournez un code public stable.
Évitez d'ajouter des bibliothèques "juste pour la commodité". Chaque validateur, helper de router ou package d'erreurs devient un style à maintenir.
Garde-fous qui empêchent la plupart des cassures :
Si vous ne pouvez pas diff deux endpoints et voir la même forme (imports, flux, gestion d'erreur), resserrez l'invite et régénérez avant de merger.
Avant de merger quoi que ce soit de généré, vérifiez d'abord la structure. Si la forme est correcte, les bugs logiques sont plus faciles à repérer.
Checks de structure :
request_id.Checks de comportement :
not found ou conflict) et confirmez le statut HTTP et la forme JSON.Traitez votre pattern comme un contrat partagé, pas une préférence. Gardez le doc « comment on build les endpoints » proche du code, et maintenez un endpoint de référence qui montre l'approche de bout en bout.
Montez la génération par petits lots. Générez 2 à 3 endpoints qui couvrent différents cas (une lecture simple, une création avec validation, une mise à jour avec un cas not-found). Puis arrêtez-vous et affinez. Si les revues détectent sans cesse la même dérive de style, mettez à jour le doc baseline et l'endpoint de référence avant de générer davantage.
Une boucle à répéter :
Si vous voulez une boucle build-review plus serrée, une plateforme vibe-coding comme Koder.ai peut vous aider à scaffolder et itérer rapidement dans un workflow chat-driven, puis exporter le code une fois qu'il correspond à votre standard. L'outil importe moins que la règle : votre baseline doit rester maîtresse.
Verrouillez un modèle répétable tôt : séparation cohérente des couches (handler → service → data access), une enveloppe d'erreur unique et une table de codes HTTP. Ensuite, utilisez un seul « endpoint de référence » que chaque nouvel endpoint doit copier.
Gardez les handlers axés sur HTTP :
Si vous voyez du SQL, des vérifications de permissions ou des branchements métier dans un handler, remontez-les au service.
Mettez les règles métier et les décisions dans le service :
Le service doit retourner des résultats domaine et des erreurs typées — pas de codes HTTP, pas de shaping JSON.
Isolez les préoccupations de persistance :
Évitez d'encoder les formats de réponse API ou d'appliquer des règles métier dans le repo au-delà de l'intégrité des données.
Un défaut simple :
Exemple : le handler vérifie que email est présent et ressemble à un email ; le service vérifie qu'il est autorisé et qu'il n'est pas déjà utilisé.
Utilisez une seule enveloppe d'erreur partout et gardez-la stable. Une forme pratique :
code pour les machines (stable)message pour les humains (court et sûr)details pour des extras structurés (comme les erreurs de champ)request_id pour le traçageCela évite les cas particuliers côté client et rend les endpoints générés prévisibles.
Écrivez une table de correspondance des codes et suivez-la systématiquement. Une séparation courante :
400 pour JSON malformé (bad_json)422 pour erreurs de validation (validation_failed)404 pour ressources manquantes (not_found)409 pour conflits (doublons/versions)500 pour échecs inattendusL'important est la cohérence : pas de débat par endpoint.
Retournez des erreurs publiques sûres et cohérentes, et loggez la cause réelle en interne.
code stable, message court, plus request_idCela évite de divulguer des internes et empêche des messages d'erreur incohérents entre endpoints.
Créez un endpoint « golden » que vous jugez fiable et exigez que les nouveaux endpoints s'y conforment :
BindJSON, WriteJSON, WriteError, etc.)Ajoutez quelques petits tests (même des table tests pour le mapping d'erreurs) pour verrouiller le pattern.
Donnez au modèle un contexte et des contraintes strictes :
Après génération, rejetez les diffs qui « améliorent » l'architecture au lieu de suivre la baseline.