go-openapi について
go-openapiは、OpenAPIを使った契約駆動のGo API開発を支援するスキルです。APIを保守する開発者やレビュアー向けに、ジェネレーター選定、コード生成、リクエスト検証、互換性確認、契約テストを扱い、仕様を唯一の情報源とします。
主な機能
- 01仕様から型・ルート・クライアントを生成
- 02ビルドとCIに生成処理を組み込む
- 03ミドルウェアでリクエスト制約を検証
- 04破壊的なAPI変更を検出
- 05生成クライアントで契約テスト
活用シーン
- 01既存のGoサービスにOpenAPI生成を導入
- 02OpenAPI駆動のAPIを拡張
- 03実装・仕様・クライアント間の整合性を確認
- 04CIで互換性のない変更を検出
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-openapiインストール後、Claude Code で/go-openapiを入力して読み込まれたか確認します。
詳細
Go OpenAPI
仕様が唯一の信頼できる情報源です。型、ルート、クライアントは仕様から生成します。仕様と並行して手書きしてはいけません。契約を手作業で二重管理すると、1 スプリント以内に食い違いが生じるためです。
運用モード
- 導入 — プロジェクトに手書きのハンドラーがあり、仕様がない、または仕様から何も生成していない場合です。書き直しをせずに生成を導入します。
- 拡張 — パイプラインがすでにある場合です。仕様を変更し、再生成してから実装します。
- レビュー — ハンドラー、仕様、公開済みクライアントが一致していること、変更によって予期せず破壊的変更が生じていないことを確認します。
1. ジェネレーターを一度だけ選ぶ
| ツール | 使用する場面 |
|---|---|
oapi-codegen | デフォルトです。型、サーバーインターフェース、クライアントを生成し、net/http、chi、echo、gin で動作します |
ogen | 完全に生成された、厳密な検証を行うサーバーが必要で、その設計方針を受け入れられる場合です |
swaggo/swag | 変換しないレガシーなコードファーストのプロジェクトに限ります。アノテーションから仕様を生成するため、コードが存在する前に仕様をレビューできません |
混在させないでください。アノテーションとチェックイン済みの仕様の両方があるプロジェクトでは、再び信頼できる情報源が二つになります。
2. ビルドに生成処理を組み込む
ジェネレーターをモジュールツールとして固定し(Go 1.24 以降)、すべての開発者と CI の実行で同じバージョンを使うようにします。
go get -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
# oapi-codegen.yaml
package: api
output: internal/api/openapi.gen.go
generate:
models: true
std-http-server: true # Go 1.22+ ServeMux; use chi-server / echo-server if that is the router
strict-server: true # typed request/response structs instead of raw http.ResponseWriter
embedded-spec: true # lets the service serve its own spec
output-options:
skip-prune: false
//go:generate go tool oapi-codegen -config oapi-codegen.yaml ../../api/openapi.yaml
生成ファイルをコミットします。レビュー担当者が契約の変更による差分を確認できるようにし、ビルドがジェネレーターのインストールに依存しないようにする必要があります。
古い生成ファイルがある場合は、CI を失敗させます。
go generate ./... && git diff --exit-code
3. 生成されたインターフェースを実装する
Strict モードでは、型付きリクエストとレスポンスが提供されるため、コンパイラーが契約を検証します。
// Generated: type StrictServerInterface interface { GetUser(ctx, GetUserRequestObject) (GetUserResponseObject, error) }
type Server struct{ users UserStore }
var _ api.StrictServerInterface = (*Server)(nil) // compile-time compliance
func (s *Server) GetUser(ctx context.Context, req api.GetUserRequestObject) (api.GetUserResponseObject, error) {
u, err := s.users.Find(ctx, req.Id)
if errors.Is(err, ErrNotFound) {
return api.GetUser404JSONResponse{Title: "user not found", Status: 404}, nil
}
if err != nil {
return nil, fmt.Errorf("find user %s: %w", req.Id, err) // 500 via the error handler
}
return api.GetUser200JSONResponse{Id: u.ID, Email: u.Email}, nil
}
ルール:
var _ api.StrictServerInterface = (*Server)(nil)をアサートします。そうすれば、仕様へのエンドポイント追加は、ステージング環境での 404 ではなくコンパイルエラーになります。- 文書化されたステータスごとに、型付きレスポンスを返します。Go の
errorを返すのは、文書化されていない失敗経路に限ります。 *.gen.goは編集しないでください。変更は必ず YAML から始めます。
4. 仕様に照らしてリクエストを検証する
生成された型で検証できるのは形式であり、制約ではありません。クエリパラメーターの minLength、pattern、enum、required は、検証ミドルウェアを追加した場合にのみ適用されます。
spec, err := api.GetSwagger()
if err != nil {
return fmt.Errorf("load spec: %w", err)
}
spec.Servers = nil // otherwise the server URL must match exactly
mux := http.NewServeMux()
handler := nethttpmiddleware.OapiRequestValidator(spec)(mux)
これにより、不正な入力は境界で拒否され、ハンドラーが実行される前に 400 が返されます。ドメインの検証はドメイン内に保ちます。ミドルウェアが適用するのは契約であり、ビジネスルールではありません。
5. エラー:RFC 9457 Problem Details
エラースキーマを一つ定義し、すべての失敗レスポンスから参照します。
components:
schemas:
Problem:
type: object
required: [type, title, status]
properties:
type: { type: string, format: uri, default: "about:blank" }
title: { type: string }
status: { type: integer }
detail: { type: string }
instance: { type: string }
application/problem+json として返します。単なる文字列は返さず、内部エラーメッセージを detail に含めないでください。ラップされたエラーはログに記録し、安定した安全なタイトルを返します。
6. バージョニングと破壊的変更
破壊的変更を機械的に検出します。レビュー担当者が見落とすことがあるためです。
go install github.com/oasdiff/oasdiff@latest
oasdiff breaking api/openapi.yaml.base api/openapi.yaml --fail-on ERR
メインブランチの仕様と比較して、これを CI で実行します。実務上の破壊的変更は、エンドポイントまたはフィールドの削除、型の制約強化、必須リクエストフィールドの追加、クライアントが理解する必要のある必須レスポンスフィールドの追加、ステータスコードの変更、レスポンスからの enum 値の削除です。
追加的な変更は安全です。破壊的変更が避けられない場合に限り、パスをバージョニング(/v2/...)し、クライアントの移行が完了するまで以前のバージョンも提供し続けます。
7. 契約テスト
実装が契約と食い違ったときに何かが失敗して初めて、仕様は契約として機能します。
func TestGetUser_MatchesSpec(t *testing.T) {
spec, err := api.GetSwagger()
require.NoError(t, err)
spec.Servers = nil
srv := httptest.NewServer(newTestHandler(t, spec))
t.Cleanup(srv.Close)
// Generated client — if the spec changed, this stops compiling
c, err := api.NewClientWithResponses(srv.URL)
require.NoError(t, err)
resp, err := c.GetUserWithResponse(t.Context(), "u-1")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
require.Equal(t, "u-1", resp.JSON200.Id)
}
テストでは、手書きの http.NewRequest ではなく、生成されたクライアントを使います。これにより、契約の不整合がコンパイルエラーになります。
8. 仕様をレビューしやすく保つ
vacuum lint -d api/openapi.yaml # or: spectral lint, redocly lint
- API ごとに
api/の下にファイルを一つ置き、チェックインして、コードと同様にレビューします。 - すべての操作に
operationIdを指定します。これは Go のメソッド名になります。 - すべてのスキーマに
descriptionを指定します。これは生成された型の godoc になります。 - 大きな仕様は
$refでcomponents/に分割します。フラグメントを生成して分割しないでください。
検証チェックリスト
- 信頼できる情報源は1つだけにします。チェックイン済みの仕様書を使用し、それと並行するアノテーションジェネレーターは使用しません
- ジェネレーターのバージョンは、
go.modの tool ディレクティブで固定します - 生成ファイルをコミットし、
go generateとgit diff --exit-codeによって CI が失敗するようにします var _ api.StrictServerInterface = (*Server)(nil)が存在すること*.gen.goを手作業で編集しないこと- リクエスト検証ミドルウェアを導入し、400 のテストでカバーすること
- エラーには単一の
Problemスキーマを使用し、application/problem+jsonとして返すこと - ベース仕様書に対して
oasdiff breakingを CI で実行すること - 生成されたクライアントで実際のハンドラーを呼び出すテストを少なくとも1つ用意すること
- 仕様書のリンティングがエラーなく完了し、すべてのオペレーションに
operationIdがあること