Claude Code を使った Go API のスキャフォールディング:ひとつの明確なハンドラー・サービス・エラーパターンを定め、Go API 全体で一貫した新しいエンドポイントを生成します。

Go の API は最初はきれいに始まります:エンドポイントが少なく、担当者も少数で、すべてが頭の中にあります。ところが API が成長し、機能がプレッシャーの下でリリースされると、小さな違いが忍び寄ります。それぞれは無害に見えても、積み重なると将来の変更を遅らせます。
よくある例:あるハンドラーは JSON を構造体にデコードして 400 を返し、別のハンドラーは 422 を違う形で返し、三つ目はログのフォーマットが違う。こうした違いはコンパイルを壊すわけではありませんが、追加作業のたびに意思決定と小さな書き換えを要求します。
混乱は次のような箇所で感じられます:
CreateUser、AddUser、RegisterUser)で検索が面倒になるここでいう「スキャフォールディング」は、新しい作業のための繰り返し可能なテンプレートを意味します:コードをどこに置くか、各レイヤーの役割、レスポンスの形。たくさんコードを自動生成することよりも、一定の形をロックすることが重要です。
Claude のようなツールは新しいエンドポイントを素早くスキャフォールドできますが、パターンをルールとして扱って初めて役に立ちます。ルールを定め、差分をレビューし、テストを回してください。モデルは標準部分を埋めますが、あなたのアーキテクチャを再定義してはいけません。
すべてのリクエストが同じ経路をたどると、Go の API は拡張しやすくなります。エンドポイントを生成する前に、レイヤーの分割方法を一つ選び、それを守ってください。
ハンドラーの仕事は HTTP のみです:リクエストを読み、サービスを呼び、レスポンスを書く。ビジネスルールや SQL、もしくは「このケースだけ特別」というロジックを含めるべきではありません。
サービスはユースケースを担当します:ビジネスルール、判断、リポジトリや外部呼び出しのオーケストレーションを行います。HTTP のステータスコードやヘッダー、エラーのレンダリング方法は知るべきではありません。
データアクセス(リポジトリ/ストア)は永続化の詳細を扱います。サービスの意図を SQL/クエリ/トランザクションに変換します。基本的なデータ整合性以外のビジネスルールを課したり、API レスポンスの形を作るべきではありません。
実用的な分離チェックリスト:
ひとつのルールを決め、曲げないでください。
シンプルなアプローチ:
例:ハンドラーは email が存在しメールらしい形式かをチェックし、サービスはそのメールが許可されているか、既に使われていないかをチェックします。
サービスがドメイン型を返すか DTO を返すかは早めに決めてください。
クリーンなデフォルトは:ハンドラーはリクエスト/レスポンス DTO を使い、サービスはドメイン型を使い、ハンドラーがドメインをレスポンスにマッピングすることです。これにより HTTP 契約が変わってもサービスは安定します。
マッピングが重いと感じる場合でも、一貫性を保ってください:サービスはドメイン型と型付きエラーを返し、JSON の整形はハンドラー側に残します。
生成されるエンドポイントを同じ作法で書かせたいなら、エラー応答を早めに固定してください。生成は出力形式が非交渉的(1つの JSON 形、1つのステータスマップ、公開するフィールドのルール)であるとき最も効果的です。
まず、失敗時にすべてのエンドポイントが返す単一のエラーエンベロープを決めます。小さく予測可能に保ちます:
{
"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..."
}
code は機械向け(安定で予測可能)に、message は人間向け(短く安全)にします。構造化データは details に入れます。バリデーションでは details.fields のようなマップが生成しやすく、クライアントが入力の横に表示するのにも便利です。
次にステータスコードマップを書き、守ってください。エンドポイントごとの議論を減らす方が良いです。もし 400 と 422 の両方を使うなら、分け方を明確にします:
bad_json -> 400 Bad Request(不正な JSON)validation_failed -> 422 Unprocessable Content(形式は正しいがフィールドが無効)not_found -> 404 Not Foundconflict -> 409 Conflict(重複キー、バージョン不一致)unauthorized -> 401 Unauthorizedforbidden -> 403 Forbiddeninternal -> 500 Internal Server Errorログに何を残し、何を返すかを決めてください。良いルールは:クライアントには安全なメッセージと request_id を返し、ログには漏らしたくない完全な内部コンテキスト(SQL、上流のペイロード、ユーザー ID)を残すことです。
最後に、request_id を標準化します。入ってきた ID ヘッダーを受け入れる(API ゲートウェイ由来なら)か、エッジ(ミドルウェア)で生成するかします。コンテキストに付与し、ログに含め、エラー応答に毎回含めてください。
スキャフォールディングを一貫させたいなら、フォルダ構成は退屈で再現可能である必要があります。ジェネレータは見えるパターンに従いますが、ファイルが散らばったり名前がフィーチャごとに変わると漂い始めます。
命名規則を一つ選び、曲げないでください。各要素の名前を一語で統一します:handler、service、repo、request、response。ルートが POST /users なら、ファイルと型名は常に users と create を中心に命名します(時々 register、時々 addUser のようにしない)。
一般的なレイヤーに合ったシンプルなレイアウト例:
internal/
httpapi/
handlers/
users_handler.go
services/
users_service.go
data/
users_repo.go
apitypes/
users_types.go
共有型の置き場所はプロジェクトが混乱するポイントです。便利なルール:
internal/apitypes に置く(JSON とバリデーションに合わせる)JSON タグが付いていてクライアント向けに設計された型は API 型として扱ってください。
ハンドラーの依存を最小にするルールを明示します:
リポジトリルートに短いパターンドキュメント(プレーンな Markdown で十分)を置いてください。フォルダツリー、命名ルール、ハンドラー→サービス→リポジトリの小さな例(どのファイルに何を書くか)を含めます。これはジェネレータに貼り付ける正確な参照になります。
十個のエンドポイントを生成する前に、信頼できる一つのエンドポイントを作ってください。これがゴールドスタンダードです:「新しいコードはこれと同じ形でなければならない」と指差せるファイルです。新規で書くか、既存のものをリファクタして基準に合わせても構いません。
ハンドラーは薄く保ちます。助けになる一手は、ハンドラーとサービスの間にインターフェースを置き、ハンドラーが具体実装ではなく契約に依存するようにすることです。
参照エンドポイントには将来ジェネレートコードが迷う箇所にだけ小さなコメントを入れてください。決定の理由(なぜ 400 と 422 を分けるか、なぜ作成は 201 を返すか、なぜ内部エラーは汎用メッセージに隠すか)を説明します。コードをただ繰り返すだけのコメントは避けます。
参照エンドポイントが動くようになったら、ヘルパーを抽出して新しいエンドポイントでドリフトが起きにくくします。よく再利用されるヘルパーは:
「薄いハンドラー + インターフェース」は実際には次のようになります:
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)
}
この形をいくつかのテスト(エラーマッピングに対するテーブルテストなど)でロックしてください。生成はひとつのクリーンなターゲットを模倣できるときに最もよく動きます。
一貫性はあなたが貼り付ける内容と禁止することから始まります。新しいエンドポイントには次の二つを与えてください:
ハンドラー、サービスメソッド、リクエスト/レスポンス型、エンドポイントが使う共有ヘルパーを含めます。その後、契約を平易に述べます:
POST /v1/widgets)命名、パッケージパス、ヘルパー関数(WriteJSON、BindJSON、WriteError、バリデータ)を必ず一致させるよう明示してください。
ゆるいプロンプトは“親切な”リファクタを許してしまいます。例えば:
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.
テストを使うなら明示的に要求してください(テストファイル名も)。そうしないとモデルはテストを省略したり、想定外のセットアップを作るかもしれません。
生成後に差分を素早く確認してください。共有ヘルパー、ルーター登録、標準エラー応答が変更されていたら出力を拒否し、「変更してはいけない」ルールをより厳しく再指定します。
出力は入力の一貫性に依存します。毎回同じプロンプトテンプレートを使い、リポジトリから小さなコンテキストスナップショットを貼るのが最速です。
貼り付けてプレースホルダを埋めてください:
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.
このテンプレートは、存在するコンテキスト、禁止事項、具体的な JSON 例を強制するために有効です。最後の指示は安全のためのキャッチです:モデルが不確かなら、コードを書く前に質問すべきです。
例えば「Create project」エンドポイントを追加するとします。目標は単純です:名前を受け取り、いくつかのルールを適用して保存し、新しい ID を返すこと。難しいのはハンドラー→サービス→リポジトリの分離と既存のエラー JSON を壊さないことです。
一貫したフローは次の通りです:
ハンドラーが受け取るリクエスト例:
{ "name": "Roadmap", "owner_id": "u_123" }
成功時は 201 Created を返します。ID の生成は一箇所に統一するのが良いです(例:Postgres に生成させ、リポジトリが返す):
{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }
現実的な失敗パスは二つ:
バリデーション失敗(名前がない、短すぎる等)はフィールドレベルのエラーとして標準形で返す:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }
名前がオーナーごとにユニークで違反が見つかった場合は 409 Conflict を返す:
{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }
パターンをきれいに保つ決定の一つ:ハンドラーは「リクエストが正しい形か」をチェックし、サービスが「許可されているか」を確認する。これにより生成されたエンドポイントが予測可能になります。
ジェネレータに自由に改良させると一貫性を失う最速の道になります。
よくあるドリフトは新しいエラー形です。あるエンドポイントは {error: "..."}、別は {message: "..."}、また別はネストを追加します。これを防ぐには、エラーエンベロープとステータスマップを一箇所に保ち、新しいエンドポイントはそれをインポートして再利用するように要求します。ジェネレータが新しいフィールドを提案したら、それは API 変更として扱ってください。
ハンドラーの肥大化もドリフトの原因です。最初は小さく始まり、徐々に権限チェック、DB クエリ、ビジネス分岐が増えます。ルールは一つ:ハンドラーは HTTP を型付きの入力と出力に変換し、サービスが意思決定を行い、データアクセスがクエリを持つこと。
命名の不一致も積み重なります。あるエンドポイントが CreateUserRequest を使い、別が NewUserPayload を使うと型を探す時間が増えます。命名規則を決め、新しい名前は強い理由がない限り拒否してください。
生のデータベースエラーをクライアントに返してはいけません。内部情報の漏洩だけでなく、エンドポイントごとにメッセージやステータスがばらばらになる原因になります。内部エラーはログに残し、公開用の安定したコードにラップしてください。
便利だからと新しいライブラリを追加するのも避けてください。バリデータ、ルータヘルパー、エラーパッケージが増えると、それぞれに合わせたスタイルを守る必要が出ます。
ドリフトを防ぐガードレール:
二つのエンドポイントを比較してインポート、フロー、エラーハンドリングが同じ形で見えないなら、プロンプトを厳しくして再生成し、マージ前に差分を通してください。
生成物をマージする前に、まず構造をチェックします。形が正しければ、ロジックのバグは見つけやすくなります。
構造チェック:
request_id の扱い動作チェック:
not found、conflict 等)を起動して HTTP ステータスと JSON 形を確認パターンは好みではなく共有契約として扱ってください。"エンドポイントの作り方" ドキュメントをコードのそばに置き、エンドツーエンドの完全なアプローチを示すリファレンスエンドポイントを維持します。
生成は小さなバッチでスケールアップします。まず異なる境界条件に触れる 2〜3 個のエンドポイントを生成(簡単な読み取り、バリデーションを伴う作成、not-found を扱う更新)し、そこで止めて精査します。レビューで同じスタイルのドリフトが繰り返し見つかるなら、ベースラインドキュメントとリファレンスエンドポイントを更新してからさらに生成します。
繰り返せるループ:
より厳密なビルド—レビューのループを望むなら、チャット駆動のワークフローでスキャフォールディングと反復を高速に行い、構造が合えばソースをエクスポートできるプラットフォーム(例:Koder.ai(koder.ai))が役立ちます。ツールは重要ではなく、ベースラインを主導権として持ち続けることが重要です。
繰り返し使えるテンプレートを早めに決めることです:一貫したレイヤー分割(handler → service → data access)、ひとつのエラーエンベロープ、ステータスコードのマップ。そして「リファレンスエンドポイント」を一つ作り、以降の新規エンドポイントはそれをコピーさせます。
ハンドラーは HTTP の仕事だけに絞るべきです:
ハンドラーの中に SQL、権限チェック、ビジネスロジックの分岐が見えたら、それはサービスへ移すべきです。
サービス層にはビジネスルールと意思決定を置きます:
サービスはドメイン結果と型付きエラーを返すべきで、HTTP ステータスや JSON の整形は含めません。
永続化はリポジトリ/データアクセスに閉じ込めます:
API レスポンス形式のエンコードやビジネスルールの実装(基礎的なデータ整合性を除く)は避けてください。
シンプルなデフォルト:
例:ハンドラーは email が存在しメールの形式になっているかを確認し、サービスはそのメールが許可されているか、既に使われていないかをチェックします。
どこでも同じエラーエンベロープを使い、その形を安定させてください。実用的な構成は:
code:機械向け(安定)message:人間向け(短く安全)details:フィールドエラーなどの構造化データrequest_id:トレース用これによりクライアント側の特別処理を避け、生成されるエンドポイントを予測可能にします。
ステータスコードのマップを文書化して毎回従うことです。よくある分け方:
400: malformed JSON(bad_json)422: バリデーションエラー(validation_failed)404: リソースが見つからない(not_found)409: 競合(重複/バージョン不一致)500: 予期しない失敗重要なのは一貫性で、エンドポイントごとに議論しないことです。
クライアントには安全で一貫した公開エラーを返し、実際の原因はログに残してください。
code、短い message、request_id内部情報の漏洩を防ぎ、エンドポイント間でバラバラなメッセージが出るのを避けます。
“ゴールデン”となる一つのリファレンスエンドポイントを作り、新しいエンドポイントはそれに合わせるようにします:
BindJSON、WriteJSON、WriteError 等)小さなテスト(エラーのマッピング表)を追加してパターンを固定してください。
生成モデルに厳格なコンテキストと制約を与えることです:
生成後、ベースラインから逸脱した差分は拒否して、ルールを厳しめに再適用してください。