grapi :  #golang で interface driven かつボイラプレートに悩まされない API 開発

[Go Conference 2018 Spring](https://gocon.connpass.com/event/82515/) にて， Go で快適に Web API 開発をするための CLI + ライブラリである [**grapi**](https://github.com/izumin5210/grapi) について話した．
本記事では，grapi で典型的なAPIをどう実装するかのワークフローとともに，grapi の特徴や思想を紹介する．

記事中では grapi v0.3.2 について扱う．



## grapi の特徴・やること

- 開発者は gRPC IDL でスキーマを定義し，gRPC server を実装する
- Ruby on Rails を意識した file generator
    - `rails new` に対応した `grapi init`
    - `rails g`に対応した， `grapi g NAME`（`grapi generate NAME`）
        - [**Google API Design Guide**](https://cloud.google.com/apis/design) に準拠
        - protobuf のスキーマや gRPC server の実装スケルトンも生成できる
        - → 開発者は server の中身の実装に集中できる
- `grapi server` を実行すればデフォルトで `:3000` に `application/json` な http server が立つ
    - ちょっと設定を変えれば `application/grpc` も話せるようになる


## プロジェクト新規作成

`grapi init APP_NAME` でプロジェクトのボイラプレートが生成される．`APP_NAME` に `.` を入れるとカレントディレクトリに生成される．
いまは2分くらいかかってるが，これは `dep ensure` がこっそり2回走っているのが原因．

[![asciicast](https://asciinema.org/a/218223.svg)](https://asciinema.org/a/218223)

生成されるファイルは以下．

- `api`
    - protobuf のスキーマ定義と生成されたコードが入る
- `app`
    - アプリケーションコードが入る
    - （`cmd/server` に合わせて `pkg/server` にしたら良かったってちょっと後悔している）
- `cmd`
    - エントリポイント（main パッケージ）が入る
    - `cmd/CMD_NAME` があるとき `grapi CMD_NAME` でコマンドが起動できる（後述）
- `tools.go`
    - [**gex**](https://github.com/izumin5210/gex) が利用する，ツール間利用マニフェストファイル
    - protoc や grapi のプラグインはこのファイルで管理されている
- `Gopkg.{toml,lock}`
    - [dep](https://golang.github.io/dep/)
    - Go Module 安定したらそっちを柄用にしたい
- `grapi.toml`
    - grapi の設定ファイル
    - `protoc` のプラグインや引数などもここに記述する

```shell-session
:) % tree -I "vendor|bin"
.
├── Gopkg.lock
├── Gopkg.toml
├── api
│   └── protos
│       └── type
├── app
│   ├── run.go
│   └── server
├── cmd
│   └── server
│       └── run.go
├── grapi.toml
└── tools.go

7 directories, 6 files
```

ここまでは大したことはしていない．


## API 定義

[![asciicast](https://asciinema.org/a/218224.svg)](https://asciinema.org/a/218224)

### API スキーマの生成

grapi の API は [gRPC の IDL](https://grpc.io/docs/guides/concepts.html#service-definition) に [`google.api.Http` で HTTP へのマッピングを記述したもの](https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#google.api.Http)で定義される．
**`grapi g service`** でそのスキーマ定義 + Go の server 実装の雛形が生成できる．また，**`grapi g scaffold-service`** を利用すると [Google API Design Guide の Standard Methods](https://cloud.google.com/apis/design/standard_methods) に則った形式の，いわゆる RESTful っぽいスキーマ定義を生成することができる．

- [生成された `api/protos/todo.proto`](https://github.com/izumin5210-sandbox/todoapp-grapi/blob/8395c6787b7224174b90b561537fd9c744dd5e7e/api/protos/todo.proto)
- [生成された `app/server/todo_server.go`](https://github.com/izumin5210-sandbox/todoapp-grapi/blob/8395c6787b7224174b90b561537fd9c744dd5e7e/app/server/todo_server.go)

ここは完全に `rails g (scaffold_)?controller` を意識している．
普通に gRPC server を実装しようとすると「`.proto` を書いて」「頑張って `protoc` の引数を組み立てて実行して」「生成された interface を実装した Go のオブジェクトを作る」までやったあとにようやくサーバの実装に取り掛かれる．
`grapi g (scaffold)?-service` だとその準備をすべてすっ飛ばして最低限の雛形ができるので，結構きもちいい．



### API スキーマの更新

grapi による API 開発における開発者のメンタルモデルは，unary な gRPC server を実装しているときとだいたい同じ，大まかにつぎの3ステップのループになるはず（厳密にはテスト書いたりいろいろあるけど，そのへんはよしなに補完してほしい）．

1. `.proto` ファイルの更新
2. `protoc` の実行
3. 実装の変更

grapi はこのうち「`protoc` の実行」について面倒を見る．**`grapi protoc`** を叩くことで `.proto` ファイル全てに対してそれぞれ必要なプラグインすべてを実行してくれる．これは先述した `grapi.toml` に記述されているとおりに実行される．

```toml
# `grapi init` で生成される `grapi.toml` の一部
# 標準では `protoc-gen-go`, `protoc-gen-grpc-gateway`, `protoc-gen-swagger` の3つが利用される

[protoc]
protos_dir = "./api/protos"
out_dir = "./api"
import_dirs = [
  "./api/protos",
  "./vendor/github.com/grpc-ecosystem/grpc-gateway",
  "./vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis",
]

  [[protoc.plugins]]
  name = "go"
  args = { plugins = "grpc", paths = "source_relative" }

  [[protoc.plugins]]
  name = "grpc-gateway"
  args = { logtostderr = true, paths = "source_relative" }

  [[protoc.plugins]]
  name = "swagger"
  args = { logtostderr = true }
```


## API 実装
### とりあえずモックデータを返す
[![asciicast](https://asciinema.org/a/MDbLjp90gIiIekbJ1C8BXG00k.svg)](https://asciinema.org/a/MDbLjp90gIiIekbJ1C8BXG00k)

`grapi init` で生成したプロジェクトでは，**`grapi server`** で API サーバを起動できる．
しかし，`grapi g (scaffold-)?service` でファイル生成した直後は，どれだけ curl しても返事がない（正確には `501 Not Implemented` が返ってくるが）．

```shell-session
$ curl localhost:3000/todos                                                                                     
{"code":12,"message":"Not Implemented"}
```

まず最初に，grapi のサーバに生成されたサーバ実装を登録する必要がある．

```diff
diff --git a/app/run.go b/app/run.go
index c9c9016..46ac33c 100644
--- a/app/run.go
+++ b/app/run.go
@@ -1,6 +1,7 @@
 package app
 
 import (
+	"github.com/izumin5210-sandbox/todoapp-grapi/app/server"
 	"github.com/izumin5210/grapi/pkg/grapiserver"
 )
 
@@ -9,7 +10,7 @@ func Run() error {
 	s := grapiserver.New(
 		grapiserver.WithDefaultLogger(),
 		grapiserver.WithServers(
-		// TODO
+			server.NewTodoServiceServer(),
 		),
 	)
 	return s.Serve()
```

ここで再度 `curl` すると，微妙に結果が変わる．

```shell-session
$ curl localhost:3000/todos                                                                                     
{"code":12,"message":"TODO: You should implement it!"}
```

これは `grapi g (scaffold-)?service` で生成された Go のコードがそういう実装になっているため．

```go
// `grapi g scaffold-service todo` で生成された Go のコードの一部
// rpc の実装はすべて `codes.Unimplemented` を返すようになっている

func (s *todoServiceServerImpl) ListTodos(ctx context.Context, req *api_pb.ListTodosRequest) (*api_pb.ListTodosResponse, error) {
	// TODO: Not yet implemented.
	return nil, status.Error(codes.Unimplemented, "TODO: You should implement it!")
}
```

ここのコードをとりあえずモックを返すように書き換えてみる．

```diff
diff --git a/app/server/todo_server.go b/app/server/todo_server.go
index eab2a9c..f71bb38 100644
--- a/app/server/todo_server.go
+++ b/app/server/todo_server.go
@@ -26,8 +26,14 @@ type todoServiceServerImpl struct {
 }
 
 func (s *todoServiceServerImpl) ListTodos(ctx context.Context, req *api_pb.ListTodosRequest) (*api_pb.ListTodosResponse, error) {
-	// TODO: Not yet implemented.
-	return nil, status.Error(codes.Unimplemented, "TODO: You should implement it!")
+	return &api_pb.ListTodosResponse{
+		Todos: []*api_pb.Todo{
+			&api_pb.Todo{TodoId: "1", Title: "Write Go 4 Advent Calendar at 2018/12/05", Done: true},
+			&api_pb.Todo{TodoId: "2", Title: "Write Go 2 Advent Calendar at 2018/12/15", Done: true},
+			&api_pb.Todo{TodoId: "3", Title: "Write Go 3 Advent Calendar at 2018/12/20", Done: true},
+			&api_pb.Todo{TodoId: "4", Title: "Write Go 3 Advent Calendar at 2018/12/20", Done: false},
+		},
+	}, nil
 }
 
 func (s *todoServiceServerImpl) GetTodo(ctx context.Context, req *api_pb.GetTodoRequest) (*api_pb.Todo, error) {
```

これで改めて `curl` をすると，それっぽい JSON が返ってくるようになる．

```
$ curl localhost:3000/todos | jq .                                                                              
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                    
                                 Dload  Upload   Total   Spent    Left  Speed                                      
100   315  100   315    0     0  45559      0 --:--:-- --:--:-- --:--:-- 52500                                     
{                                                                                                                  
  "todos": [                                                                                                       
    {                                                                                                              
      "todo_id": "1",                                                                                              
      "title": "Write Go 4 Advent Calendar at 2018/12/05",                                                         
      "done": true                                                                                                 
    },                                                                                                             
    {                                                                                                              
      "todo_id": "2",                                                                                              
      "title": "Write Go 2 Advent Calendar at 2018/12/15",                                                         
      "done": true                                                                                                 
    },                                                                                                             
    {                                                                                                              
      "todo_id": "3",                                                                                              
      "title": "Write Go 3 Advent Calendar at 2018/12/20",                                                         
      "done": true                                                                                                 
    },                                                                                                             
    {                                                                                                              
      "todo_id": "4",                                                                                              
      "title": "Write Go 3 Advent Calendar at 2018/12/20"                                                          
    }                                                                                                              
  ]                                                                                                                
}
```

grapi が面倒を見てくれるのはここまで: API Desing Guide に従ったコード生成と `protoc` のラップ，あとはコマンドの実行とビルドだけ．
実際のアプリケーション開発はここからが本番になるが，その**「本番」に至るまでの瑣末事を肩代わりするのが grapi の仕事**になる．


## おまけ
### DB をつなぎ込む
実際にデータ永続化とかをするときは，`*sql.DB` などのコネクションなりクライアントなりを `fooServiceServerImpl` struct にもたせると良い．
ちょっと古い & 内容が異なるサンプルだけど，[Go + grpc-gateway でつくる JSON API 速習会 @ Wantedly](https://wantedly.connpass.com/event/82378/) で利用したサンプルリポジトリの [sqlx 導入 PR](https://github.com/wantedly/grpc-gateway-study/pull/2) が参考になると思う．

アプリケーションが大きくなってくると server に `*sql.DB` を直接もたせるんじゃなくて，DAO や repository などの抽象化パターンの導入を検討するといい．


### gRPC を使う

`grapi init` した直後のプロジェクトは普通に `localhost:3000` に `application/json` で話しかけると返事をしてくれる．
これは内部で2つのサーバを動かすことで実現されている．

- gRPC server
    - unix domain socket（デフォルトだと `tmp/server.sock`）を listen
- [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) server
    - tcp の `:3000` を listen
    - gRPC client デフォルトだと `tmp/server.sock` にある gRPC server を見に行く

これらのサーバがどこを listen するかは，以下に挙げる grapi の起動オプションで切り替えることが可能．

- [grapiserver.WithGrpcAddr](https://godoc.org/github.com/izumin5210/grapi/pkg/grapiserver#WithGrpcAddr)
- [grapiserver.WithGrpcInternalAddr](https://godoc.org/github.com/izumin5210/grapi/pkg/grapiserver#WithGrpcInternalAddr)
- [grapiserver.WithGatewayAddr](https://godoc.org/github.com/izumin5210/grapi/pkg/grapiserver#WithGatewayAddr)
- [grapiserver.WithAddr](https://godoc.org/github.com/izumin5210/grapi/pkg/grapiserver#WithAddr)

当然だけど，grpc-gateway を経由せず gRPC server と直接 `application/grpc` で通信することもできる．

なので，「今後 gRPC を導入していきたいのでアプリケーションレイヤでの知見がほしい・IDL を先行利用していきたい」みたいなユースケースでとりあえず grapi を使っておいて，サーバ・クライアント・インフラすべての準備が整ったタイミングで gateway を剥がす…という使い方も可能（というか，まさにその用途で使いたくて grapi を作った）．


## 既存のパッケージ・ツールとの比較


[gin](https://github.com/gin-gonic/gin) や [echo](https://github.com/labstack/echo), `net/http.Server` を使った実装との比較のメリットとしては，やはり `json.Unmarshal` をしなくていいことに尽きると思っている，ちゃんとした struct の形でパラメタが渡ってくるのが一番嬉しい．これはコード生成ベースでやることの大きな強みである．

[goa](https://github.com/goadesign/goa) は同じくコード生成ベース・interface driven な開発ができるパッケージである．これに対する強みとしては前述した通り「gRPC 移行への前段階として利用できる」ほか，「protobuf は実装言語に依存しない IDL で記述するので，多言語・プラットフォームからの利用も簡単」というのは大きいはず．

生 gRPC server との比較としては…，「gRPC をいつでも使いはじめられる環境」が既にあるのなら grpc-gateway を通さずに使うべきである．grpc-gateway を使わないとしても，grapi のもつ Google API Design Guide に則ったボイラプレート生成は有効に使えると思う．また，この「gRPC をいつでも使いはじめられる環境」というのは実は結構難度が高い（インフラ的な障壁, スキーマ共有どうする, 生成コードはどう扱う, etc）．なので，そこまですぐに用意できない・だけど近い内に gRPC を導入しておきたい という状況のときには grpc-gateway を内包する grapi は強い味方になると思う．

ちなみに，「IDL から（サーバ・クライアントの）コード生成」というのは microservices architecture かつサービス数が増えれば増えるほど欲しくなってくる．なので，そういう状況に置かれてる現場では gRPC の投入を見越しつつ grapi の採用を検討してもらえると嬉しい．


## 雑感
grpc-gateway は直感的でわかりやすいんだけど，むだに json -> pb 変換が挟まるので用途によってはパフォーマンスが気になるかもしれない．
grpc-gateway 以外の，でも gRPC は使わないアプローチとしては

- [reflection protocol](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) をうまく使ったやり方（e.g. [mercari/grpc-http-proxy](https://github.com/mercari/grpc-http-proxy)）を考える
- protobuf をしゃべる http client を生成 & protobuf をそのまま proxy するだけの server を作る
- protobuf IDL から `http.Server` & HTTP クライアントを生成

等があるかもしれない．
一方で，protobuf & gRPC まわりはかなりエコシステムが発展してきているので，最終的には生で gRPC を利用できるに越したことはないはず．