解決したい課題
OpenAPIでのspecとコード開発が分離されていると以下のような課題がある
- 利用者が見ると仕様と実装のズレが起きる可能性
- ズレが起きないとしても開発時の仕様確認や慎重なレビューが面倒
oapi-codegenを使うことでspec通りの実装を強制できる。 https://github.com/deepmap/oapi-codegen/tree/master
oapi-codegenを使ったAPI開発のステップ
- (A) OpenAPI定義を作成
- (B) oapi-codegen で (A) からGo言語APIのコード生成
- (C) (B)に含まれるサーバのインターフェース(StrictServerInterface)を実装
- (D) main関数から(C)をリクエストハンドラとして利用しているWebフレームワークに登録
ここでは以下の条件に沿って例を示す。
- oapi-codegenのexampleにあるOpenAPI仕様を利用
- Webフレームワークはechoを使用
今回の完成品の全体感は以下。
. ├── api // API controllers │ ├── api.gen.go // (B)generated API handler + server interface │ ├── api.go // (C) server implementation │ ├── server.yml // (B)config to generate api.gen.go │ ├── type.gen.go // (B) generated types │ └── type.yml // (B) config to generate type.gen.go ├── go.mod ├── go.sum ├── main.go (D) └── openapi.yaml // (A) source of api.gen.go & type.gen.go
コードは以下に置いてある。 https://github.com/yoshikipom/go/tree/main/oapi-codegen
(A) OpenAPI定義を作成
今回は以下をそのまま使う。自分で書くならVS Codeの拡張やStoplight Studioを使う。 https://github.com/deepmap/oapi-codegen/blob/master/examples/petstore-expanded/petstore-expanded.yaml
(B) oapi-codegen で (A) からGo言語APIのコード生成
yamlファイルでoapi-codegenの生成ルールを定義 server.yml
package: api generate: echo-server: true strict-server: true embedded-spec: true output: api/api.gen.go
type.yml
package: api generate: models: true output: api/type.gen.go
これらをoapi-codegenで読み込んで API関連のコードであるapi/api.gen.go と リクエスト・レスポンスなどの型情報であるapi/type.gen.go を生成。
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest oapi-codegen -config api/type.yml openapi.yaml oapi-codegen -config api/server.yml openapi.yaml
(C) (B)に含まれるサーバのインターフェース(StrictServerInterface)を実装
自分はVSCodeの拡張でコードを生成してから中身を埋めている。
生成されるコード
// Returns all pets // (GET /pets) func (ss *MyStrictServer) FindPets(ctx context.Context, request api.FindPetsRequestObject) (api.FindPetsResponseObject, error) { panic("not implemented") // TODO: Implement } // Creates a new pet // (POST /pets) func (ss *MyStrictServer) AddPet(ctx context.Context, request api.AddPetRequestObject) (api.AddPetResponseObject, error) { panic("not implemented") // TODO: Implement } ...
中身の実装例。RequestBodyがAddPetRequestObject.Body
にbindingされていることや、Pet型を戻り値として渡せることから型安全かつ仕様通りに実装できることがわかる。また、このレイヤですでにWebフレームワークに依存しない実装になっているのも良い。
func (ss *MyStrictServer) AddPet(ctx context.Context, request AddPetRequestObject) (AddPetResponseObject, error) { var pet Pet pet.Name = request.Body.Name pet.Tag = request.Body.Tag pet.Id = ss.NextId ss.NextId = ss.NextId + 1 ss.Pets[pet.Id] = pet return AddPet200JSONResponse(pet), nil }
(D) main関数から(C)をリクエストハンドラとして利用しているWebフレームワークに登録
e.Use(middleware.OapiRequestValidator(swagger))
でリクエストのバリデーションを行うmiddlewareを登録api.RegisterHandlers(e, h)
でechoに(C)で実装したハンドラを登録
package main import ( "flag" "fmt" "os" "github.com/deepmap/oapi-codegen/pkg/middleware" "github.com/labstack/echo/v4" echomiddleware "github.com/labstack/echo/v4/middleware" "github.com/yoshikipom/go/oapi/api" ) func main() { var port = flag.Int("port", 8080, "Port for test HTTP server") flag.Parse() swagger, err := api.GetSwagger() if err != nil { fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err) os.Exit(1) } swagger.Servers = nil e := echo.New() e.Use(echomiddleware.Logger()) e.Use(middleware.OapiRequestValidator(swagger)) ss := api.NewMyStrictServer() h := api.NewStrictHandler(ss, []api.StrictMiddlewareFunc{}) api.RegisterHandlers(e, h) e.Logger.Fatal(e.Start(fmt.Sprintf("0.0.0.0:%d", *port))) }
動作確認
サーバサイド
yoshiki@yoshiki-mbp:go/oapi-codegen ‹main›$ go run main.go ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.10.2 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:8080 {"time":"2023-07-04T23:39:23.773179+09:00","id":"","remote_ip":"127.0.0.1","host":"localhost:8080","method":"POST","uri":"/pets","user_agent":"vscode-restclient","status":200,"error":"","latency":121657,"latency_human":"121.657µs","bytes_in":43,"bytes_out":41}
クライアント
yoshiki@yoshiki-mbp:go/oapi-codegen ‹main›$ curl --request POST \ --url http://localhost:8080/pets \ --header 'content-type: application/json' \ --header 'user-agent: vscode-restclient' \ --data '{"name": "Alice","tag": "test2"}' {"id":1001,"name":"Alice","tag":"test2"}
OpenAPI Generator はどうか
OpenAPI Generator https://github.com/OpenAPITools/openapi-generator これ一つでOpenAPI定義から多様な言語のサーバー側、クライアント側のコードを生成できる優れもの。 ただ、保守性の面で不安がある。oapi-codegen の場合は生成されたファイルに手を加える必要がないが、OpenAPI Generatorの場合は生成されたファイルに手を加えて拡張する必要がある。例えば、main.goも生成されるが、middlewareの追加などを手動で行うことはよくあるので手動での変更は必須である。これだと仕様変更時にファイルを再生成場合、手動でマージする必要が出てしまう。 Go言語のAPI開発だけ考えるのであれば oapi-codegen を選択する。
感想
クライアントサイドの生成と違って、サーバサイドの生成はメンテが難しそうで抵抗があったが、このツールで生成したコードはロジックの実装と完全に分離されているので非常に使いやすそう。REST APIを新規開発する機会があれば導入してみたい。