はじめに
- 決められたフォーマットで記述したGoの構造体からを自動生成するジェネレータがv1.0.0迎えたからアウトプットする回(12/31時点でv1.6.1)
- コントローラーを生成するserver_generator、API疎通するためのTypeScriptを生成するclient_generatorの2種類がある
- 基本的には構造体を書くだけ
- (beta) 生成後のコントローラーにある程度swaggoに対応したgodocも挿入されている為、調整すればSwaggerコードも自動生成できる
- リポジトリ(go-generalize/api_gen)
こんな方向け
- Goコード正義でAPIコントローラー/クライアントを自動生成したい
- エンジニア間でバックエンドとフロントエンドのI/Fを合わせるのがめんどくさい
- バックエンドでルーティング、フロントエンドでAPIリクエストを実装ごとに書くのが辛い
こんな人達に向いている
とりあえず試す
前提: こちらのディレクトリに居てほしい~~(雑)~~
各バイナリを落とす
$ make bootstrap
コード生成
$ make generate
生成に関してはこれで終わり
backend/とfrontend/以下に様々なコードが生成されていることが確認できれば成功
詳細は後述
サーバージェネレータ
server_generator に含まれる内容
構造体名ルール
-
{HTTP_METHOD}Name{Request|Response}
- 必ずRequestとResponseがそれぞれ対になるようにする
対応HTTPメソッド
-
GET
,POST
,PUT
,DELETE
,PATCH
パスルーティングをする場合
- ディレクトリは
_
始まり- 例:
/service/:serviceID/hogehoge
にしたい場合は/service/_serviceID/*.go
のようにする
- 例:
- ファイルは
0_
始まり- 例:
/service/:serviceID
にしたい場合は/service/0_service_id.go
のようにする
- 例:
実行
$ server_generator ./hoge/
mockオプション(後述)
$ server_generator -mock ./hoge/
サーバージェネレータ使用例
#####今回使用するソースコード
今回の構成
$ tree backend/interfaces
backend/interfaces
├── api
│ ├── user
│ │ ├── 0_user_id.go
│ │ ├── _userID
│ │ │ └── age_increment.go
│ │ └── search.go
│ └── user.go
└── health_check.go
3 directories, 5 files
GET /health_check
type GetHealthCheckRequest struct{}
type GetHealthCheckResponse struct {
Status string `json:"status"`
}
PUT /api/user
type PutUserRequest struct {
Name string `json:"name" validate:"required,min=3,max=10,excludesall=!()#@{}"`
Age int `json:"age" validate:"required,gt=0,lte=150"`
Gender model.Gender `json:"gender" validate:"required,oneof=1 2 3"`
}
type PutUserResponse struct {
Status int `json:"status"`
User *model.User `json:"payload,omitempty"`
}
GET /api/user/search
type GetSearchRequest struct {
Name string `json:"name" query:"name"`
Age int `json:"age" query:"age"`
Gender model.Gender `json:"gender" query:"gender"`
}
type GetSearchResponse struct {
Status int `json:"status"`
User []*model.User `json:"payload,omitempty"`
Messages []werror.FailedReason `json:"messages"`
}
GET/PATCH/DELETE /api/user/:userID
type GetRequest struct {
ID string `json:"userID" param:"userID" validate:"required"`
}
type GetResponse struct {
Status int `json:"status"`
User *model.User `json:"payload,omitempty"`
Messages []werror.FailedReason `json:"messages"`
}
type PatchRequest struct {
ID string `json:"userID" param:"userID" validate:"required"`
Name string `json:"name,omitempty" validate:"min=5,max=10,excludesall=!()#@{}"`
Age int `json:"age,omitempty" validate:"gt=0,lte=150"`
Gender model.Gender `json:"gender,omitempty" validate:"oneof=1 2 3"`
}
type PatchResponse struct {
Status int `json:"status"`
User *model.User `json:"payload,omitempty"`
Messages []werror.FailedReason `json:"messages"`
}
type DeleteRequest struct {
ID string `json:"userID" param:"userID" validate:"required"`
}
type DeleteResponse struct {
Status int `json:"status"`
Messages []werror.FailedReason `json:"messages"`
}
POST /api/user/:userID/age_increment
type PostAgeIncrementRequest struct {
ID string `json:"userID" param:"userID" validate:"required"`
}
type PostAgeIncrementResponse struct {
Status int `json:"status"`
User *model.User `json:"payload,omitempty"`
Messages []werror.FailedReason `json:"messages"`
}
生成
$ make server_generate
`生成後tree`
$ tree backend/interfaces
backend/interfaces
├── api
│ ├── put_user_controller_gen.go
│ ├── routes_gen.go
│ ├── user
│ │ ├── 0_user_id.go
│ │ ├── _userID
│ │ │ ├── age_increment.go
│ │ │ ├── post_age_increment_controller_gen.go
│ │ │ └── routes_gen.go
│ │ ├── delete_controller_gen.go
│ │ ├── get_controller_gen.go
│ │ ├── get_search_controller_gen.go
│ │ ├── patch_controller_gen.go
│ │ ├── routes_gen.go
│ │ └── search.go
│ └── user.go
├── bootstrap_gen.go
├── get_health_check_controller_gen.go
├── health_check.go
├── props
│ └── controller_props.go
├── routes_gen.go
└── wrapper
├── internal
│ └── fmt.go
└── wrapper.go
6 directories, 20 files
生成コントローラ(一部)
// Package _userID ...
// generated version: 1.6.1
package _userID
import (
"github.com/54m/api_gen-example/backend/interfaces/props"
"github.com/labstack/echo/v4"
)
// PostAgeIncrementController ...
type PostAgeIncrementController struct {
*props.ControllerProps
}
// NewPostAgeIncrementController ...
func NewPostAgeIncrementController(cp *props.ControllerProps) *PostAgeIncrementController {
p := &PostAgeIncrementController{
ControllerProps: cp,
}
return p
}
// PostAgeIncrement ...
// @Summary WIP
// @Description WIP
// @Accept json
// @Produce json
// @Param userID path string true "user id"
// @Success 200 {object} PostAgeIncrementResponse
// @Failure 400 {object} wrapper.APIError
// @Failure 500 {object} wrapper.APIError
// @Router /api/user/{userID}/age_increment [POST]
func (p *PostAgeIncrementController) PostAgeIncrement(
c echo.Context, req *PostAgeIncrementRequest,
) (res *PostAgeIncrementResponse, err error) {
// API Error Usage: github.com/54m/api_gen-example/backend/interfaces/wrapper
//
// return nil, wrapper.NewAPIError(http.StatusBadRequest)
//
// return nil, wrapper.NewAPIError(http.StatusBadRequest).SetError(err)
//
// body := map[string]interface{}{
// "code": http.StatusBadRequest,
// "message": "invalid request parameter.",
// }
// return nil, wrapper.NewAPIError(http.StatusBadRequest, body).SetError(err)
panic("require implements.") // FIXME require implements.
}
最終的には こうなりました
クライアントジェネレータ
client_generator に含まれる内容
- Typescript + fetchを利用したライブラリが生成される
- server_generator向けの用意したフォルダに対して実行する
- ライブラリは今いるディレクトリに対して生成される
生成後
$ tree frontend/client_generated
frontend/client_generated
├── api_client.ts
└── classes
├── api
│ ├── types.ts
│ └── user
│ ├── _userID
│ │ └── types.ts
│ └── types.ts
└── types.ts
4 directories, 5 files
クライアントジェネレータ使用例
import { APIClient } from "./client_generated/api_client";
// the simplest
(async () => {
const client = new APIClient();
const resp = await client.getHealthCheck(/* param */ {});
console.log(resp);
})();
// with options
(async () => {
const client = new APIClient(
/* token */
"pass", // [optional] token for Authorization: Bearer
/* commonHeaders */
{
"X-Foobar": "foobar", // [optional] custom headers
},
/* baseURL */
"http://localhost:8888", // [optional] custom endpoint
/* commonOptions */
{
cache: "no-cache", // [optional] options for fetch API
},
);
const resp = await client.api.putUser(
/* param */
{
name: "54m",
age: 100,
gender: 1,
},
/* header */
{
"X-Foobar": "hoge", // [optional] custom headers
},
/* options */
{
mode: "cors" // [optional] options for fetch API
},
);
console.log(resp);
})();
よくある質問
Q. とりあえずReq/Resの構造体が出来てサーバー↔クライアント間で疎通出来るようになったけどバックエンド実装がない時、フロント側の開発は待たないとダメじゃね?
A. そんなことは無い
api_genにはモックサーバが用意されており正しく使えばバックエンドとフロントエンドが並行で開発出来る
今回用意したサンプルだと次のように扱う
`サーバー`
$ make server_generate_with_mock
何をしたのか
$ server_generator -mock ./interfaces
このように引数に -mock
を付ければ良い
mock_jsons/
mock_routes_gen.go
mock_bootstrap_gen.go
backend/interfaces/cmd/mock/main.go
これらのファイルが追加で自動生成される
モックサーバー実行方法
$ make run_mock
モックサーバは生成対象としたディレクトリの直下に
cmd/mock/main.go
として生成される
モックサーバを起動するためにはビルドオプションで
-tags mock
を付ける必要がある
go run -tags mock backend/interfaces/cmd/mock/main.go -port 8888
`クライアント`
import { APIClient, MockOption } from "./client_generated/api_client";
// mock mode
(async () => {
const client = new APIClient();
const mockOption: MockOption = {
wait_ms: 10,
target_file: 'default.json'
}
const resp = await client.api.user.getSearch(/* param */ {
name: "54m",
age: 100,
gender: 1,
}, /* header */ undefined, /* options */ {
'mock_option': mockOption,
});
console.log(resp);
})();
モックサーバに関するドキュメント(server) / モックサーバに関するドキュメント(client)
Q. Cookieを送信したい
A. {credentials: "include"}
をfetch APIのオプションに設定 (FYI)
まとめ
APIのI/Fが自動生成されるだけで幸せになれました(?)
まだまだ開発途中なので、Issue / PR お待ちしております!