概要
Go言語のWebフレームワークのgoa@v3とDDD(ドメイン駆動設計)を用いて、Web APIを実装したときの備忘録です。
ご指摘等があればご教授いただけると幸いです。
本記事は前編後編に分かれており、以下でお送りします。
前編:goaについての説明/実装
後編:DDDによる実装
goaとは
以下、公式ドキュメントを引用します。
goa は、独立したサービスを扱う際に時間を節約し、システム全体の一貫性を維持するのに役立つ、マイクロサービスを開発するための新しいアプローチを提供します。 goa は、ドキュメント、クライアントモジュール、クライアントツールなどの定型的な成果物と補助的な成果物の両方を処理するためにコード生成を使用します。生成される成果物の内容はマイクロサービスの デザイン から計算されます。したがって、デザインは信頼できる唯一の情報源(Single Source of Truth)であり、そこから API 実装、ドキュメンテーション、クライアントおよびその他の生成される成果物が導出されます。設計の変更はサービスのメンテナンスを大幅に簡素化して自動的に反映されます。
要約するとDSL(ドメイン特化言語)を用いて、WebAPIのサービスを書くことで、APIの実装とドキュメントに乖離が出ないように進めることができるフレームワークです。
goaを採用した理由
昨今では、WebAPIを開発をする際にさまざまなフレームワークを選択することができますが、弊社では大きく2つの理由からgoaを採用しました。
・ドキュメンテーションをしなくて良いこと/乖離しないこと
・DDDとの相性が良いこと
ドキュメンテーションをしない
WebAPIを実装する際に、API仕様書を作成すると思います。フロント側はAPI仕様書をもとにAPIの繋ぎ込みをすると思いますが、これが間違っていると非常に嫌です。また、ドキュメンテーションというはそもそも面倒な作業で誰もしたくないです。これが解決できるだけでかなり効率が良いです。
goaでは一番最初にDSLによってAPIデザインを行い、そこから自動生成されるコードとAPI仕様書(OpenAPI.yaml)が出力される流れになっています。よって、いやでもコードとAPIの仕様書が一致する仕組みになっています。
go-swaggerなどのスキーマベースでの開発も考えましたが、せっかくGoで書くならコードベースの方が良いだろうと判断しました。
DDDとの相性が良い
詳しくは後編で説明しますが、goaで自動生成されるコードはDDDでいうとInterface層にあたります。これによりビジネスロジック部分の実装に注力することができます。(DDD初心者の僕も安心!!)
技術選定には、DMM様の記事が非常に参考になりました。
実装手順
それでは、実装していきましょう。今回使用しているのは、goa@v3です。v1,v2では書き方が異なることがあるので注意が必要です。
今回のAPIの題材は簡単にユーザーの登録とユーザーの取得にしましょう。
type User struct {
Id int
Name string
Age int
Email string
FriendId []int
}
GET /api/user #全件取得
GET /api/user/{id} #特定のidのユーザー情報を取得
POST /api/user #ユーザー登録
goaのお作法
まず、rootディレクトリにdesignパッケージを作成します。これはgoaのお作法になります。変更もできますが、する必要も特にないと思ってます。
.
├── design
│ └── design.go
├── go.mod
└── go.sum
design.goの書き方
design.goには主に3つの要素で書きます。(もちろんもっと詳細に書く必要はありますが)
- API
- Service/Method
- Type
API
APIメソッドでは、APIに対する設定を書くことができます。ここに記載された情報はOpenAPI.yamlに出力された際に情報として書かれるので、詳細に書いておくことをお勧めします。
package design
import (
. "goa.design/goa/v3/dsl"
)
var _ = API("api-server", func() {
Title("API Server")
Description("REST API Server")
})
Service/Method
DSLの肝となる部分です。サービスその名の通りサービスを設定します。ここでエンドポイントやメソッド、リクエスト/レスポンスの設定、エラーの設定などを記載します。
Resultメソッドに含まれるUserType
については後述します。
var _ = Service("User", func() {
Description("user情報のサービス")
//サービス全体のエンドポイントを設定
HTTP(func() {
Path("/api/user")
})
//各メソッドを定義
Method("ユーザー情報の全件取得", func() {
Description("ユーザー情報の全件取得")
// ペイロードの定義
Payload(func() {
Attribute("name", String, "名前", func() {
MaxLength(50)
Example("山田")
})
})
//レスポンスの定義
Result(ArrayOf(UserType))
//メソッドの設定
HTTP(func() {
Params(func() {
Param("name")
})
GET("")
})
})
})
注目するべきはMethod
と呼ばれる関数です。
このMethod
では基本的には三段階に分かれてます。
- メソッド内で使用できるペイロード情報(リクエスト)の定義
- レスポンスの定義
- メソッドの設定(エンドポイントやリクエストの設定、GETやPOSTなどのHTTPメソッドを設定)
Payloadメソッドはパスパラメーター
/クエリパラーメーター
/ボディ
の全てをここで定義します。
そのため、下段のHTTPメソッドにて、どれがパスパラメーターで、クエリパラメーターなのかを書く必要があります。
仮にHeaderに認証用Tokenが必要な場合は、以下のように記載しましょう。
Method("ユーザー情報の全件取得", func() {
Description("ユーザー情報の全件取得")
// パラメーターの定義
Payload(func() {
+ Attribute("token", String, "認証用Token", func() {
+ Example("ABCD")
+ })
Attribute("name", String, "名前", func() {
MaxLength(50)
Example("山田")
})
})
//レスポンスの定義
Result(ArrayOf(UserType))
//メソッドの設定
HTTP(func() {
+ Headers(func() {
+ Header("token")
+ })
Params(func() {
Param("name")
})
GET("")
})
})
それはで、別のメソッドも追加していきましょう。特定のユーザーの取得する際に、IDをパスパラメーターに含めるのを忘れないようにしましょう。
var _ = Service("User", func() {
Description("user情報のサービス")
HTTP(func() {
Path("/api/user")
})
Method("ユーザー情報の全件取得", func() {
...
})
+ Method("特定のユーザー情報の取得", func() {
+ // パラメーターの定義
+ Payload(func() {
+ Attribute("id", Int, "id", func() {
+ Example(2)
+ })
+ })
+ //レスポンスの定義
+ Result(UserType)
+
+ //メソッドの設定
+ HTTP(func() {
+ GET("/{id}")
+ })
+ })
+
+ Method("ユーザー情報の登録", func() {
+ // パラメーターの定義
+ Payload(func() {
+ Attribute("body", UserType, "ユーザー情報")
+ Required("body")
+ })
+ //レスポンスの定義
+ Result(String)
+
+ //メソッドの設定
+ HTTP(func() {
+ POST("")
+ Body("body")
+ })
+ })
)}
Type
途中で解説を飛ばしていたUserType
についてについて説明します。
Typeはいわゆるスキーマにあたります。goaではスキーマを定義し、それを使い回すことができます。OpenAPIをyamlで書いたことがある方なら少しは馴染みがあると思います。
var UserType = Type("User", func() {
Description("ユーザーのタイプ")
Attribute("id", Int, "ユーザーID", func() {
Example(1)
})
Attribute("name", String, "名前", func() {
MaxLength(50)
Example("山田太郎")
})
Attribute("age", Int, "年齢", func() {
Minimum(15)
Maximum(150)
Example(20)
})
Attribute("email", String, "メールアドレス", func() {
Format(FormatEmail)
Example("neccos@email.com")
})
Attribute("friend_id", ArrayOf(Int), "友達のID", func() {
Example([]int{1, 2, 3})
})
Required("id", "name", "age", "email")
})
一番下の行のRequiredメソッドでは、必須かどうかのバリデーションを書けることができます。
Required("id", "name", "age", "email")
各要素に対してもバリデーションを書けることができます。goaが優秀だなと思うのは、設定した型に対してExampleが間違っているとコード自動生成時にエラーを出してくれます。
Attribute("id", Int, "ユーザーID", func() {
Example("test") //example value "1" is incompatible with attribute of type int in attribute
})
さらに、定義したUserTypeを拡張したTypeを作成することもできます。
ここでは、管理者権限を付与されたユーザーを考えます。
var AdminUserType = Type("AdminUser", func() {
Description("管理者ユーザーのタイプ")
Attribute("role", Int, "権限ロール 1:Super 2:admin", func() {
Enum(1, 2)
Example(1)
}
Extend(UserType)
})
以上、基本的なメソッドを書きましたが、他にもミドルウェアを差し込んだりエラーレスポンスを作成したりすることができるので、以下の公式ドキュメント、GoDocsを参照してください。
コードを自動生成
goaには自動生成コードをする際のコマンドが主に2つあります。
- goa example パッケージ名/design
- goa gen パッケージ名/design
goa example
goa example
はdesignで設定したサーバー情報やミドルウェア、エンドポイントの生成コードを自動で生成してくれます。また、上記で図解したInterface層のコードも(一応)出力してくます。
goa example your_project/design
tree
.
├── cmd
│ ├── your_api_name
│ │ ├── http.go
│ │ └── main.go
│ └── ayour_api_name-cli
│ ├── http.go
│ └── main.go
├── design
│ └── design.go
├── go.mod
├── go.sum
このコマンドは既存ファイルの上書きはしてくれないため、基本的には最初の一回のみを叩きます。
この後説明する、goa genコマンドと叩けば、これでサーバーとして動きます。また、exampleするとcliまで出力してくれます。非常に便利だなと触れた時は思いました。
goa gen your_project/design
go run ./cmd/your_api_name && go run ./cmd/your_api_name-cli
[apiserver] 18:52:27 HTTP "GetUserList" mounted on GET /api/user
[apiserver] 18:52:27 HTTP "GetUser" mounted on GET /api/user/{id}
[apiserver] 18:52:27 HTTP "PutUser" mounted on POST /api/user
[apiserver] 18:52:27 HTTP server listening on "localhost:8080"
懸念点
弊社のプロジェクトではこのコマンドは一度のみしか叩きません。さらに、一度叩いた後にcmd/your_api_name以外のファイルを削除します。プロジェクトに応じてはyour_api_nameの中のコードも修正する必要があるでしょう。(弊社でもAWS Lambda用にカスタマイズしてたりします)
理由は以下の通りです。
- rootフォルダにコードが生成されてしまうから
- cliを特に使わないから
- 上書きされることがないので、結局自身で修正しなければならない。
特に一番上のrootフォルダにコードが生成されてしまうのは大問題です。GoのPackgeシステムに干渉してエラーなどを吐くことがあります。また、DDDの思想を取り入れるとなるとinterfaceパッケージの中に格納したかったので、使用はしない方針です。それでも、goaにはあまりある魅力があると思います。(そもそも 名前からしてもサンプルですし!!)
goa gen
このコマンドはでDSLで記載したデザインをもとにコードを自動生成してくれるかつドキュメントを生成してくれます!!
designファイルを修正したら忘れずにgoa genしましょう。このコマンドはgenディレクトリ
を毎回新規作成してくれます。
実行するとopenapi.yamlとopenapi.jsonが出力されます。(感動!!)
jsonファイルも出力されることで、フロント側でopenapi-generatorを使用して型を生成するということも弊社では行なっています。
genディレクトリ内に生成されているファイルを使用して、DDDに活用していきますがその部分については後編でお話ししようと思います。
goa gen パッケージ名/design
.
├── cmd
├── design
├── *gen
│ ├── http
│ ├── openapi.json
│ ├── openapi.yaml
│ ├── openapi3.json
│ └── openapi3.yaml
├── go.mod
└── go.sum
最後に
前編の「~DSLを書こう~」は以上になります。goaと言っても、DSLを書くことが中心になると思いますが、ドキュメントとそれに沿ったバリデーションなどが実装されたコードが自動で生成されるのは非常にいいなと思いました。
少しDSLの書き方にくせ(というか妥協)がある場合がありますが、それでも導入をすることでかなりメリットがあると思います。
【後編】goa@v3とDDDでWebAPI ~ドメイン層を書こう~