はじめに
今回は Go 言語で OpenAPI のコード自動生成を試してみたいと思います。「コード自動生成って何だ?」という方や「OpenAPI ってそもそも何?」という方にもわかりやすく解説していこうと思います。
まず、OpenAPI でコード自動生成ができると以下の嬉しいことがあります。
- ただの作業になりがちなモデルの作成が自動化できる
- 仕様書通りにモデルや API インターフェースが自動生成されるので、バグが入りにくい
- 仕様書通りのリクエスト・レスポンスかどうかを簡単にバリデーションできる
- その結果、ビジネスロジックに集中する開発ができる
です。
今回のサンプルコードはこちらに置いてあります。
OpenAPI とは
OpenAPI とは、 REST API の定義を記述する仕様のことです。例えば、以下は「GET /users
API を叩いたら200ステータスで id, name を返す」という定義を OpenAPI 仕様で書いたものです。
openapi: 3.0.3
info:
version: 0.0.1
title: サンプル API
paths:
/users:
get:
operationId: GetUser
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
仕様を詳しく知らなくても直感的になんとなく理解できるものになっていると思います。
このようにプログラミング言語に依存しない決まったインターフェースを定義することにより、人間もコンピュータもアクセスしやすくなります。
その OpenAPI の定義を基に Swagger と呼ばれるツールで API 仕様書が自動生成できたり、REST API のプログラムコードまで自動生成ができるのです。
以下は Swagger で自動生成された API 仕様書の例です。
Go + Echo で試してみる
この OpenAPI のコード自動生成を Go + Echo で試してみたいと思います。
Echo は Go 言語の代表的な WEB アプリケーションフレームワークです。 Echo は軽量かつ高速でなおかつ書き味も良く、1番人気になりつつあります。また、 REST API に最適化しているため、今回のようなちょっとした API 開発にとても便利です。
OpenAPIのジェネレーターを使うと、その Go + Echo の API のコードを自動生成してくれるのです。
ちなみに、Go で OpenAPI を扱うには oapi-codegen というコードジェネレーターを使うのが一般的です。
いろいろ登場人物が出てきたので、整理しながらインストールから順を追ってやっていきましょう。
環境
- MacOS
- Homebrew
- VSCode
事前準備(1) インストール
必要なものをインストールしていきます。
Go のインストールがまだであれば Homebrew でインストールします。
インストール後に「パスを通せ」と案内が出るので、その通りにパスも通しておきましょう。
$ brew install go
続いて Go の OpenAPI ジェネレーターをインストールします。
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest
以上でインストールするものは終わりです。
事前準備(2) VSCode 設定
VSCode で OpenAPI 仕様を書く際、以下の拡張機能を入れておくと素敵になれます。
- Swagger Viewer
-
Shift
+option
+P
で右側に Swagger Editor と同様の画面が開きます。
-
- openapi-lint
- OpenAPI の静的解析ツールです。凡ミスを防いでくれます。
では早速 API を作っていきましょう。
(1) Go のプロジェクト作成
まずはプロジェクトを作成しておきます。プロジェクト名は何でも構いません。
$ go mod init sample-go-echo-openapi
(2) OpenAPI 定義
続いて yaml で OpenAPI の定義を書いてみます。
例として user の情報を扱う、以下のようなAPIを定義してみます。
-
GET /users
- リクエストに成功した場合ステータスコード200と
User
スキーマを返す - リクエストに失敗した場合エラーを返す
- リクエストに成功した場合ステータスコード200と
-
POST /users
- リクエストに成功した場合ステータスコード200を返す
- リクエストに失敗した場合エラーを返す
openapi: 3.0.3
info:
version: 0.0.1
title: サンプル API
paths:
/users:
get:
operationId: GetUser
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/User"
default:
description: デフォルトのエラーレスポンス
content:
application/json:
schema:
$ref: "#/components/schemas/DefaultErrorResponse"
post:
operationId: PostUser
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/User"
responses:
"200":
description: OK
default:
description: デフォルトのエラーレスポンス
content:
application/json:
schema:
$ref: "#/components/schemas/DefaultErrorResponse"
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
required:
- id
- name
DefaultErrorResponse:
type: object
properties:
message:
type: string
注目するところは User
と DefaultErrorResponse
を components
に切り出している点です。
共通化の観点では当たり前のことですが、 components
に切り出しておくことでコード自動生成の際にこのモデルを自動で作ってくれます。
ではこの定義を基に Go のコードをジェネレートしてみましょう。
# ジェネレート先のディレクトリ作成
$ mkdir generated
# コードジェネレーターを使ってコード自動生成
$ oapi-codegen -package generated ./api.yaml > ./generated/api.go
# パッケージの依存関係を解決
$ go mod tidy
./generated/api.go
に Go のコードが生成されました。何も指定しないとデフォルトで Echo 用のコードが生成されます。
少し中身を除いてみましょう。
まずはServerInterface というインターフェースが作られており、API として実装しなくてはならないメソッドがあらかじめ決められています。そして API が受け取る全てのパラメーターは echo.Context
に格納されてくるのがわかります。これはとても便利ですね。
// ServerInterface represents all server handlers.
type ServerInterface interface {
// (GET /users)
GetUser(ctx echo.Context) error
// (POST /users)
PostUser(ctx echo.Context) error
}
User
モデルや DefaultErrorResponse
の定義も自動生成されています。
// DefaultErrorResponse defines model for DefaultErrorResponse.
type DefaultErrorResponse struct {
Message *string `json:"message,omitempty"`
}
// User defines model for User.
type User struct {
Id int `json:"id"`
Name string `json:"name"`
}
その他にサーバ側だけでなくクライアント側のコードも自動生成されていることがわかります(今回はサーバ側のコードしか使いません)。サーバとクライアントの双方が Open API を利用すれば、より強力なツールになりますね。
// The interface specification for the client above.
type ClientInterface interface {
// GetUser request
GetUser(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
// PostUser request with any body
PostUserWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
PostUser(ctx context.Context, body PostUserJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
}
(3) Echo + OpenAPI で REST API を書く
では自動生成された OpenAPI のコードを使って API を作っていきます。
api
フォルダを作りそこに main.go
というファイルで以下のコードを書きます。
内容は簡単なのでコード中にコメントを残してあります。
package main
import (
"fmt"
"net/http"
oapi "sample-go-echo-openapi/generated"
oapiMiddleware "github.com/deepmap/oapi-codegen/pkg/middleware"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type apiController struct{}
// OpenAPI で定義された (GET /users) の実装
func (a apiController) GetUser(ctx echo.Context) error {
// OpenApi で生成された User モデルを使ってレスポンスを返す
return ctx.JSON(http.StatusOK, &oapi.User{
Id: 1,
Name: "Taro Yamada",
})
}
// OpenAPI で定義された (POST /users) の実装
func (a apiController) PostUser(ctx echo.Context) error {
// リクエストボディを構造体にバインド
user := &oapi.User{}
ctx.Bind(&user)
fmt.Println(user)
// 200 ステータスのみ返す
return ctx.NoContent(http.StatusOK)
}
func main() {
// Echo のインスタンス作成
e := echo.New()
// OpenApi 仕様に沿ったリクエストかバリデーションをするミドルウェアを設定
swagger, err := oapi.GetSwagger()
if err != nil {
panic(err)
}
e.Use(oapiMiddleware.OapiRequestValidator(swagger))
// ロガーのミドルウェアを設定
e.Use(middleware.Logger())
// APIがエラーで落ちてもリカバーするミドルウェアを設定
e.Use(middleware.Recover())
// OpenAPI の仕様を満たす構造体をハンドラーとして登録する
api := apiController{}
oapi.RegisterHandlers(e, api)
// 8080ポートで Echo サーバ起動
e.Logger.Fatal(e.Start(":8080"))
}
コードを書いたら go mod tidy
を打ってパッケージの依存を解決しておきましょう。
早速実行してみます。
$ go run api/main.go
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.9.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
⇨ http server started on [::]:8080
Echo のサーバが 8080
ポートで立ち上がりました。「Echo」の文字が格好良いです。
curl で API を叩いてみます。
# GET /users
$ curl http://localhost:8080/users
{"id":1,"name":"Taro Yamada"}
# POST /users
$ curl -X POST -H "Content-Type: application/json" -d '{"id":2, "name":"Hanako Yamada"}' -i http://localhost:8080/users
HTTP/1.1 200 OK
Date: Sun, 11 Dec 2022 13:51:19 GMT
Content-Length: 0
ちゃんとレスポンスが返ってきました!
サーバ側のログを見てみると...
{"time":"2022-12-12T11:21:56.445806+09:00","id":"","remote_ip":"127.0.0.1","host":"localhost:8080","method":"GET","uri":"/users","user_agent":"curl/7.84.0","status":200,"error":"","latency":113666,"latency_human":"113.666µs","bytes_in":0,"bytes_out":30}
&{2 Hanako Yamada}
{"time":"2022-12-12T11:21:57.99662+09:00","id":"","remote_ip":"127.0.0.1","host":"localhost:8080","method":"POST","uri":"/users","user_agent":"curl/7.84.0","status":200,"error":"","latency":228208,"latency_human":"228.208µs","bytes_in":32,"bytes_out":0}
リクエストログが出ているのがわかります。
そして fmt.Println
で表示したように、POST で送った json リクエストボディもちゃんと取れていることがわかります。
では、次にリクエストのバリデーションがちゃんと効いているか確認してみましょう。
POST リクエストで間違った json リクエストを送ってみます。
$ curl -X POST -H "Content-Type: application/json" -d '{"hoge":"fuga"}' -i http://localhost:8080/users
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=UTF-8
Date: Sun, 11 Dec 2022 13:55:31 GMT
Content-Length: 112
{"message":"request body has an error: doesn't match the schema: Error at \"/id\": property \"id\" is missing"}
400エラーが出て弾かれました。ちゃんとバリデーションも効いています!
バリデーションのコードを書かなくても OpenAPI の定義に沿って勝手にやってくれるのはとてもありがたいですね。
まとめ
Go + Echo + OpenAPI で少ないコードで REST API が作れました。
また、API インターフェースやモデルが自動生成されるため、バグが入りにくいという安心感があります。
何より、ビジネスロジックに集中した開発ができるのが嬉しいところです。
サーバ側とクライアント側が OpenAPI 仕様に沿って開発できれば、より強力なツールとなることでしょう。