はじめに
コードの自動生成をすると、手軽にAPIサーバの起動をすることができました。
備忘録も兼ねて手順を紹介したいと思います。
oapi-codegenのインストール法は別の記事にまとめているので、OpenAPIのyamlファイルとoapi-codegenでGoの自動生成をやってみたを参照してください。
やったこと
yamlファイルの作成
Goおよびoapi-codegenのインストールが終わっているならば、yamlファイルを作成します。
これが仕様書の代わりにもなります。
作成した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プロパティは後で生成するコードの関数名として使用されるので、重複しない名前を考えるようにしましょう。
コード生成
yamlファイルの作成ができれば、コードを生成してみましょう。
自動生成したコードであることがわかるように、generatedディレクトリに保存し、openapi.gen.go
というファイル名とします。
自動生成に使用するconfig.yaml
も作成しておきます。
package: handler # 任意の名前でOK
output: ./generated/openapi.gen.go # generatedディレクトリが存在しないと失敗する
generate:
echo-server: true
models: true
output-options:
skip-prune: true
公式を真似ただけですが、とりあえずこれで十分です。
generateプロパティをいじることでecho以外のサーバーに指定することができます。
自動生成を行うコマンドは以下のとおりです。
oapi-codegen -config config.yaml openapi.yaml
-config config.yaml
は設定ファイルを指定しています。
openapi.yaml
の箇所は自動生成に使用するyamlファイルを指定します。
自動生成されるコードは以下に記載しておきます。
生成コード
// 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)
}
ルーティングを設定してくれていますし、リクエストレスポンスの型も作成してくれています。
パスパラメータが存在するか確認もしてくれているので、後は処理を書くだけで起動ができる状態です。
生成したコードを直接変更すると、再度コード生成を行う際に上書きしてしまう恐れがあります。
なので、実際に使用するファイルにコピペしておきます。
私はhandlerディレクトリを作成し、handler.go
としておきました。
main.goの作成
自動生成したコードにはmain関数がないので作成していきます。
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"))
}
go.modの作成/importの調整
go.modファイルを作成します。
go mod init
上記コマンドで作成できるはずですが、上手く作成できない場合はサンプルのgo.modを作成しましょう。
下記のコマンドで作成できます。
go mod init example.com/m/v2
go mod tidy
go.mod
で指定されるmodule名を確認しておきましょう
module Go_Sample_Server
module名はimportに使用します。
任意の名前で良いですが、module名を変更したらmain.goのimportも修正するようにしましょう。
import(
"モジュール名/ディレクトリ名"
)
とすることで指定したディレクトリ内のファイルをimportできます。
importしたファイルにある関数を呼び出すときは、importファイルで指定したpackage名.関数名
とすればよいです。
APIサーバ起動
go run main.go
上記コマンドで起動できます。
以下の用にEchoの出力が出れば起動しています。
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.12.0
後はcurlを叩いたり、フロントから接続したり、openapi.yamlに新たなエンドポイントを作成してみてください。
最後に
コードを自動生成することで、設計やAPIが行う処理に注力することができます。
あくまで、手軽にサーバ起動までできることを紹介したかったので、セキュリティであったり詰めが甘い箇所があると思いますが、ご容赦ください。
今回はGoの自動生成を行いましたが、他の言語も試してみたいと思います。