go-api-design について
サービスの契約を保守する開発者向けに、Go の HTTP API とハンドラー設計を扱うスキルです。標準ライブラリのルーティング、検証、エラー形式、ページネーション、ミドルウェア、正常終了を解説します。
主な機能
- 01HTTP ハンドラーと ServeMux の規約
- 02ミドルウェア順序とリクエスト検証
- 03URL、命名、ページネーションの指針
- 04一貫したエラーレスポンス形式
- 05正常終了とヘルスチェック
活用シーン
- 01Go HTTP サービスのルート設計
- 02リクエスト検証とエラーレスポンスのレビュー
- 03API のページネーションと終了動作の計画
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-api-designインストール後、Claude Code で/go-api-designを入力して読み込まれたか確認します。
同梱ファイルreferences/handlers-and-middleware.md
詳細
Go API 設計
API は契約です。一度公開すれば、それは約束になります。10 年間保守するつもりで設計してください。実際にそうなる可能性が高いからです。
コミュニティのデフォルトです。
go-api-designを明示的に上書きするプロジェクトのスキルまたはエージェント設定ルールがある場合は、そちらが優先されます。
詳細なリファレンス資料は、必要に応じて読み込みます。
references/handlers-and-middleware.md— ハンドラー構造体、JSON レスポンスヘルパー、リクエスト ID およびリカバラーミドルウェア、デコードと検証。
以下の概要だけでは不十分な場合に限り、リファレンスファイルを読んでください。
1. HTTP ハンドラーの構造
- 標準の
http.ServeMux(Go 1.22 以降)でルーティングします。"GET /users/{id}"パターンはメソッドに一致し、r.PathValue("id")でワイルドカードを読み取れます。メソッドが異なる場合はAllowとともに 405 が返されます。ルーター依存関係を導入する価値があるのは、ミドルウェアグループやサブルーターのためであり、パスパラメーターのためではありません。 - ハンドラーは依存関係(ストア、ロガー)を保持する構造体のメソッドにします。パッケージグローバルにはしません。
- ハンドラーメソッドは何も返しません。レスポンスを書き込み、エラーを自身で処理し、ドメインエラー(
errors.Is(err, ErrNotFound))をステータスコードにマッピングして、予期しないエラーのみをログに記録します。 Content-Typeを設定し、ステータスを一度だけ書き込み、エンコードの失敗をログに記録する、単一の JSON ヘルパーを経由してルーティングします。
2. ミドルウェアのパターン
ミドルウェアは func(http.Handler) http.Handler です。順序は外側から内側へ次のとおりです。
Recoverer → RequestID → Logger → Auth → RateLimit → Handler
Recover は必ず最外側に配置します。認証はビジネスロジックより前に行います。ロガーは処理時間を記録します。
3. リクエストの検証
- デコード前にボディのサイズを制限します。
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)。 - 型付きリクエスト構造体にデコードし、同じステップで検証します。デコードに成功しても、値が有効とは限りません。
- 安定したメッセージとともに
400を返します。デコーダーのエラーテキストをそのまま返してはいけません。
4. URL と命名規則
GET /api/v1/users → list users
POST /api/v1/users → create user
GET /api/v1/users/{id} → get user
PUT /api/v1/users/{id} → replace user
PATCH /api/v1/users/{id} → partial update
DELETE /api/v1/users/{id} → delete user
GET /api/v1/users/{id}/orders → list user orders (nested resource)
ルール:
- リソースには複数形の名詞を使います。
/userではなく/users。 - 複数語のパスにはケバブケースを使います。
/order-items。 - API 内で使う JSON フィールドの規則は一つに統一し、混在させません。ここでは camelCase(
"createdAt")がデフォルトです。既存の API がある場合は、それに合わせてください。 - URL パスにバージョンを含めます。
/api/v1/...。 - URL に動詞を含めません。
/searchUsersではなく/users/search?q=alice。
5. ページネーション
type PageRequest struct {
Cursor string `json:"cursor"`
Limit int `json:"limit"`
}
type PageResponse[T any] struct {
Items []T `json:"items"`
NextCursor string `json:"nextCursor,omitempty"`
HasMore bool `json:"hasMore"`
}
大規模なデータセットでは、offset/limit よりカーソルベースのページネーションを優先します。offset ページネーションは、並行書き込みがあると破綻します。
6. グレースフルシャットダウン
func main() {
if err := run(context.Background()); err != nil {
slog.Error("server stopped", slog.Any("error", err))
os.Exit(1)
}
}
func run(ctx context.Context) error {
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadHeaderTimeout: 5 * time.Second, // Slowloris guard
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 120 * time.Second,
}
errc := make(chan error, 1)
go func() { errc <- srv.ListenAndServe() }()
select {
case err := <-errc:
return fmt.Errorf("listen: %w", err) // e.g. port already in use
case <-ctx.Done():
}
// ctx is already cancelled; shutdown needs its own deadline.
shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("shutdown: %w", err)
}
if err := <-errc; !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
ReadHeaderTimeoutは Slowloris 攻撃への防御策です。これがないと、クライアントはヘッダーバイトを少しずつ送信し続けて接続を開いたままにできます。これが設定されていないサーバーには、gosecが G112 の警告を出します。http.ErrServerClosedとの比較には==ではなく、必ずerrors.Isを使います。- 終了するのは
mainだけにします。サーバー処理の goroutine 内でlog.Fatalを呼ぶと、シャットダウン処理を含むすべての defer によるクリーンアップが実行されません。
7. ヘルスチェックエンドポイント
// Liveness: is the process alive? GET /healthz → 200, no dependency checks
// Readiness: should it receive traffic? GET /readyz → 200 or 503
func (h *HealthHandler) handleReady(w http.ResponseWriter, r *http.Request) {
if h.draining.Load() { // atomic.Bool, set when shutdown starts
h.respondError(w, http.StatusServiceUnavailable, "shutting down")
return
}
if err := h.db.PingContext(r.Context()); err != nil {
h.respondError(w, http.StatusServiceUnavailable, "database unavailable")
return
}
h.respondJSON(w, http.StatusOK, map[string]string{"status": "ready"})
}
ロードバランサーの背後で SIGTERM を受けたら、draining を設定し、ロードバランサーがルーティングを停止するまで readiness の期間分だけ待ってから、srv.Shutdown を呼び出します。先にシャットダウンすると、ロードバランサーがまだ送信中のリクエストを拒否してしまいます。liveness には依存関係のチェックを含めません。データベースの障害によってオーケストレーターがすべてのレプリカを再起動する事態を避けるためです。
8. エラーレスポンス形式
API 全体で一貫したエラーレスポンスを使用します。
{
"error": {
"code": "VALIDATION_ERROR",
"message": "invalid request parameters",
"details": [
{"field": "email", "message": "must be a valid email"}
]
}
}
ハンドラーの境界で内部エラーを HTTP ステータスコードにマッピングします。内部エラーをクライアントに決して漏らしてはいけません。標準的な形式には、RFC 9457 の problem details(application/problem+json)を使います。型は go-openapi スキルにあります。
9. 冪等性とブラウザークライアント
- 作成や課金を行う
POSTエンドポイントでは、Idempotency-Keyヘッダーを受け付けます。キーからレスポンスへの対応を一定期間保存し、同じキーが再度使われた場合は保存済みのレスポンスを再生し、同時に発生した重複リクエストは 409 で拒否します。クライアントやプロキシは再試行します。これがないと、再試行によって二重課金が発生します。 - Cookie を使ってブラウザーから呼び出される API には CSRF 保護が必要です。
http.CrossOriginProtection(Go 1.25 以降)を使用します。詳細は go-security-audit スキルが担当します。