oapi-codegenを使ってGoのコード自動生成とAPIサーバーの起動までやってみた

# はじめに

コードの自動生成をすると、手軽にAPIサーバの起動をすることができました。
備忘録も兼ねて手順を紹介したいと思います。

oapi-codegenのインストール法は別の記事にまとめているので、[OpenAPIのyamlファイルとoapi-codegenでGoの自動生成をやってみた](https://qiita.com/SBS_Takumi/items/8bcf5cb957b06b1633d5)を参照してください。

[コード全体像Githubリポジトリ](https://github.com/IamSBStakumi/Go_Sample_Server/tree/Qiita-Sample)

# やったこと

## yamlファイルの作成
Goおよびoapi-codegenのインストールが終わっているならば、yamlファイルを作成します。
これが仕様書の代わりにもなります。

<details open><summary>作成したyamlファイル</summary>

```openapi.yaml
openapi: "3.0.3"
info:
  version: 0.0.1
  title: REST API
servers:
  - url: http://localhost:9000
tags:
  - name: user
  - name: version
paths:
  /:
    get:
      summary: "バージョン確認"
      operationId: getVersion
      tags:
        - version
      responses:
        "200":
          description: OK
  /register:
    post:
      summary: "新規ユーザー登録"
      operationId: registerUser
      tags:
        - user
      responses:
        "201":
          description: OK
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  example: sample@test.com
                password:
                  type: string
                  format: password
                  example: Hoge1357
              required:
                - email
                - password
  /user/{firebase_uid}:
    delete:
      summary: "ユーザーの削除"
      operationId: deleteUser
      tags:
        - user
      parameters:
        - name: firebase_uid
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: user {firebase_uid} deleted

```

APIのエンドポイント、HTTPメソッド、リクエストボディやレスポンスを定義しています。
operationIdプロパティは後で生成するコードの関数名として使用されるので、重複しない名前を考えるようにしましょう。

</details>

## コード生成

yamlファイルの作成ができれば、コードを生成してみましょう。
自動生成したコードであることがわかるように、generatedディレクトリに保存し、`openapi.gen.go`というファイル名とします。

自動生成に使用する`config.yaml`も作成しておきます。

```config.yaml
package: handler # 任意の名前でOK
output: ./generated/openapi.gen.go # generatedディレクトリが存在しないと失敗する
generate:
  echo-server: true
  models: true
output-options:
  skip-prune: true
```
公式を真似ただけですが、とりあえずこれで十分です。
generateプロパティをいじることでecho以外のサーバーに指定することができます。

自動生成を行うコマンドは以下のとおりです。

```bash
oapi-codegen -config config.yaml openapi.yaml
```
`-config config.yaml`は設定ファイルを指定しています。
`openapi.yaml`の箇所は自動生成に使用するyamlファイルを指定します。

自動生成されるコードは以下に記載しておきます。

<details><summary>生成コード</summary>


```openapi.gen.go
// Package api provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.16.2 DO NOT EDIT.
package handler

import (
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/oapi-codegen/runtime"
	openapi_types "github.com/oapi-codegen/runtime/types"
)

// RegisterUserJSONBody defines parameters for RegisterUser.
type RegisterUserJSONBody struct {
	Email    openapi_types.Email `json:"email"`
	Password string              `json:"password"`
}

// RegisterUserJSONRequestBody defines body for RegisterUser for application/json ContentType.
type RegisterUserJSONRequestBody RegisterUserJSONBody

// ServerInterface represents all server handlers.
type ServerInterface interface {
	// バージョン確認
	// (GET /)
	GetVersion(ctx echo.Context) error
	// 新規ユーザー登録
	// (POST /register)
	RegisterUser(ctx echo.Context) error
	// ユーザーの削除
	// (DELETE /user/{firebase_uid})
	DeleteUser(ctx echo.Context, firebaseUid string) error
}

// ServerInterfaceWrapper converts echo contexts to parameters.
type ServerInterfaceWrapper struct {
	Handler ServerInterface
}

// GetVersion converts echo context to params.
func (w *ServerInterfaceWrapper) GetVersion(ctx echo.Context) error {
	var err error

	// Invoke the callback with all the unmarshaled arguments
	err = w.Handler.GetVersion(ctx)
	return err
}

// RegisterUser converts echo context to params.
func (w *ServerInterfaceWrapper) RegisterUser(ctx echo.Context) error {
	var err error

	// Invoke the callback with all the unmarshaled arguments
	err = w.Handler.RegisterUser(ctx)
	return err
}

// DeleteUser converts echo context to params.
func (w *ServerInterfaceWrapper) DeleteUser(ctx echo.Context) error {
	var err error
	// ------------- Path parameter "firebase_uid" -------------
	var firebaseUid string

	err = runtime.BindStyledParameterWithLocation("simple", false, "firebase_uid", runtime.ParamLocationPath, ctx.Param("firebase_uid"), &firebaseUid)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter firebase_uid: %s", err))
	}

	// Invoke the callback with all the unmarshaled arguments
	err = w.Handler.DeleteUser(ctx, firebaseUid)
	return err
}

// This is a simple interface which specifies echo.Route addition functions which
// are present on both echo.Echo and echo.Group, since we want to allow using
// either of them for path registration
type EchoRouter interface {
	CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
	DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
	GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
	HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
	OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
	PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
	POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
	PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
	TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
}

// RegisterHandlers adds each server route to the EchoRouter.
func RegisterHandlers(router EchoRouter, si ServerInterface) {
	RegisterHandlersWithBaseURL(router, si, "")
}

// Registers handlers, and prepends BaseURL to the paths, so that the paths
// can be served under a prefix.
func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) {

	wrapper := ServerInterfaceWrapper{
		Handler: si,
	}

	router.GET(baseURL+"/", wrapper.GetVersion)
	router.POST(baseURL+"/register", wrapper.RegisterUser)
	router.DELETE(baseURL+"/user/:firebase_uid", wrapper.DeleteUser)

}
```
</details>

ルーティングを設定してくれていますし、リクエストレスポンスの型も作成してくれています。
パスパラメータが存在するか確認もしてくれているので、後は処理を書くだけで起動ができる状態です。

生成したコードを直接変更すると、再度コード生成を行う際に上書きしてしまう恐れがあります。
なので、実際に使用するファイルにコピペしておきます。
私はhandlerディレクトリを作成し、`handler.go`としておきました。

## main.goの作成

自動生成したコードにはmain関数がないので作成していきます。

<details open><summary>main.go</summary>

```main.go
package main

import (
	"net/http"

    // 自動生成したコードをインポート
	"Go_Sample_Server/handler"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

type Server struct {}

// handler.goから呼び出される関数の処理を定義
func (h Server) GetVersion(ctx echo.Context) error {
	return ctx.JSON(http.StatusOK, "0.0.1")
}

func (h Server) RegisterUser(ctx echo.Context) error {
	return ctx.JSON(http.StatusCreated, "Success!")
}

func (handle Server) DeleteUser(ctx echo.Context, firebaseUid string) error{
	return ctx.JSON(http.StatusOK, "User deleted")
}

func main(){
	e := echo.New()

	// middleware
	e.Use(middleware.Recover())
	e.Use(middleware.Logger())
    // CORS設定
	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		// TODO: Frontのアドレスに変更する
		AllowOrigins: []string{"http://localhost:5173"},
        // 許可するHTTPメソッドを設定
		AllowMethods: []string{http.MethodGet, http.MethodPatch, http.MethodPost, http.MethodDelete, http.MethodOptions},
	}))

	server := Server{}


	handler.RegisterHandlers(e, server)

	e.Logger.Fatal(e.Start("localhost:9000"))
}
```
</details>


## go.modの作成/importの調整

go.modファイルを作成します。
```bash
go mod init
```
上記コマンドで作成できるはずですが、上手く作成できない場合はサンプルのgo.modを作成しましょう。
下記のコマンドで作成できます。
```bash
go mod init example.com/m/v2
go mod tidy
```

`go.mod`で指定されるmodule名を確認しておきましょう

```go.mod
module Go_Sample_Server
```

module名はimportに使用します。
任意の名前で良いですが、module名を変更したらmain.goのimportも修正するようにしましょう。

```go.main
import(
    "モジュール名/ディレクトリ名"
)
```

とすることで指定したディレクトリ内のファイルをimportできます。
importしたファイルにある関数を呼び出すときは、`importファイルで指定したpackage名.関数名`とすればよいです。

## APIサーバ起動

```bash
go run main.go
```

上記コマンドで起動できます。
以下の用にEchoの出力が出れば起動しています。

```bash
  ____    __
 / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.12.0
```

後はcurlを叩いたり、フロントから接続したり、openapi.yamlに新たなエンドポイントを作成してみてください。

## 最後に

コードを自動生成することで、設計やAPIが行う処理に注力することができます。

あくまで、手軽にサーバ起動までできることを紹介したかったので、セキュリティであったり詰めが甘い箇所があると思いますが、ご容赦ください。

今回はGoの自動生成を行いましたが、他の言語も試してみたいと思います。