LoginSignup
11
3

More than 1 year has passed since last update.

[Go] OpenAPI コード自動生成でビジネスロジックに集中する開発へ

Last updated at Posted at 2022-12-18

はじめに

今回は 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 仕様書の例です。
スクリーンショット 2022-12-12 10.21.22.png

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 の静的解析ツールです。凡ミスを防いでくれます。

以下は Swagger Viewer を使った画面です。
スクリーンショット 2022-12-11 21.41.58.png

では早速 API を作っていきましょう。

(1) Go のプロジェクト作成

まずはプロジェクトを作成しておきます。プロジェクト名は何でも構いません。

$ go mod init sample-go-echo-openapi

(2) OpenAPI 定義

続いて yaml で OpenAPI の定義を書いてみます。
例として user の情報を扱う、以下のようなAPIを定義してみます。

  • GET /users
    • リクエストに成功した場合ステータスコード200と User スキーマを返す
    • リクエストに失敗した場合エラーを返す
  • POST /users
    • リクエストに成功した場合ステータスコード200を返す
    • リクエストに失敗した場合エラーを返す
api.yaml
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

注目するところは UserDefaultErrorResponsecomponents に切り出している点です。
共通化の観点では当たり前のことですが、 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 仕様に沿って開発できれば、より強力なツールとなることでしょう。

11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3