GolangのgoaでAPIをデザインしよう(基本編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
goでAPIを作成する場合は、必ずといっていいくらいgoaでやっている私ですが、日本ではあまり使っている人が居ません。恐らくこの理由は日本語の情報があまり無いことやシンプルなサンプル集がないからかもしれない・・・と思いまして、軽量なサンプルを定期的に紹介していくことにしました。
API設計フェーズ
goaは最初に設計を行ってから、その設計書を元に実装を行っていきます。設計はDSLと呼ばれるもので、最初はこの定義に慣れないですが、読み方さえ分かれば容易にAPIを設計出来ます。
何が嬉しいの?
例えば、GET /users/:ID
というエンドポイントがあるとします。どのようなロジックを組む必要があるでしょう?
例 curl http://localhost/users/1
でリクエストする
/users/1
の1の部分を取得する- :IDから取得したものが文字なのか数字なのかチェックする
- 取得した:IDを使って、データベースなりにSELECT句などで検索する
- 該当するものがなかったら、404ステータスを返す
- 正しく取得できたら、200ステータスとレコードを返す。
goaだとどうなるか?
/users/1
の1の部分を取得する:IDから取得したものが文字なのか数字なのかチェックする- 取得した:IDを使って、データベースなりにSELECT句などで検索する
- 該当するものがなかったら、404ステータスを返す
- 正しく取得できたら、200ステータスとレコードを返す。
1,2に当たる値チェックなどが一切必要なくなります。上記のように少ないパラメータならそこまでの労力ではありませんが、正規表現が必要だったり、複数だったりすると・・・面倒ですよね?
つまり、goaはビズネスロジックオンリーの開発が出来るようになるので、人が組むコードの部分が非常にシンプルになります。
しかも、swaggerのドキュメントが自動生成されるので、ドキュメント更新地獄から解放されます。
フォルダ構成
以下のようなファイル構成を構築してください。
※designフォルダ内のファイルは実際は一つでもokですが、分ける方が私は好きです。
. ├── LICENSE ├── Makefile ├── README.md ├── design │ ├── api_definition.go │ ├── media_types.go │ └── resources.go └── public └── swagger
goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。
ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。
go get github.com/tikasan/goa-stater または ghq get github.com/tikasan/goa-stater
デザインする
準備が整ったので、さっそくデザインをしていきたいと思います。
今回はとりあえず動かそうということで、サンプルソースをひたすら貼ります。
細かい説明は順を追って記事を作成して、紹介致します。
APIの基本情報(api_definition.go)
APIの実際の動作ではなく、どういったAPIか?やグローバルな設定などを行います。ここに紹介しているもの以外にも関数は存在します。
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = API("goa simple sample", func() { // APIのタイトル Title("tikasan/goa-simple-sample") // APIの説明 Description("goaのサンプルです") // 作成者へのコンタクト情報 Contact(func() { Name("pei") Email("satak47cpc@gmail.com") URL("https://github.com/tikasan/goa-simple-sample/issues") }) // APIのライセンス License(func() { Name("MIT") URL("https://github.com/tikasan/eventory/blob/master/LICENSE") }) // APIのドキュメント Docs(func() { Description("wiki") URL("https://github.com/tikasan/goa-simple-sample/wiki") }) // ホストの設定 Host("localhost:8080") // 対応しているプロトコル定義、httpかhttpsまたはその両方 Scheme("http", "https") // 全てのエンドポイントのベースパス // /usersというエンドポイントがあったら、/api/v1/usersとなる BasePath("/api/v1") // CORSポリシーの定義 Origin("http://localhost:8080/swagger", func() { //クライアントに公開された1つ以上のヘッダー Expose("X-Time") // 1つまたは複数の許可されたHTTPメソッド Methods("GET", "POST", "PUT", "DELETE") //プリフライト要求応答をキャッシュする時間 MaxAge(600) // Access-Control-Allow-Credentialsヘッダーを設定する Credentials() }) })
MediaType(media_types.go)
レスポンスデータの形式を定義します。(詳しいことは別記事を紹介予定)
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) // レスポンスデータの定義 // MediaTypeに名前をつけます var IntegerMedia = MediaType("application/vnd.integer+json", func() { // 説明 Description("example") // どのような値があるか(複数定義出来る) Attributes(func() { // idはInteger型 Attribute("id", Integer, "id", func() { // 返すレスポンスの例 Example(1) }) // レスポンスに必須な要素(基本は全て必須にした方が楽) Required("id") }) // 返すレスポンスのフォーマット(別記事で紹介予定) View("default", func() { Attribute("id") }) })
リソースへの操作(resources.go)
リソースへの操作を定義します。(詳しいことは別記事を紹介予定)
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) // /actionsの定義をする var _ = Resource("actions", func() { // actionsリソースのベースパス BasePath("/actions") /* リソースに対してどういった操作を行うか定義する add リソースを追加する list リソースをリストで取得する delete リソースを削除する 上記のような感じで定義すればおkです。 */ Action("ping", func() { // アクションの説明 Description("サーバーとの導通確認") Routing( // エンドポイント -> GET http://localhost/api/v1/actions/pingになる GET("/ping"), ) // 返したいレスポンス // 200 OK + MessageTypeで定義しているMediaType Response(OK, MessageType) // 400 BadRequest + ErrorMediaというデフォルトで容易されているMediaType // 足りないパラメーターなどがあれば自動的に返される Response(BadRequest, ErrorMedia) }) Action("hello", func() { Description("挨拶する") Routing( GET("/hello"), ) // リクエストで付加出来るパラメーター Params(func() { // nameという名前でパラメーターをStringでなげれる Param("name", String, "名前", func() { // もし、空だった場合は空文字を格納する Default("") }) // 必ず設定されるべきパラメーター(デフォルト値があるので、存在しなければ空になる) Required("name") }) Response(OK, MessageType) Response(BadRequest, ErrorMedia) }) Action("ID", func() { Description("複数アクション(:id)") Routing( // エンドポイントにリソースを指定出来る // GET http://localhost:8080/api/v1/actions/1になる GET("/:id"), ) Params(func() { // :IDはIntegert型でなければならない。 Param("id", Integer, "id") // Requiredはリソースを含めたエンドポイントになるので、定義しなくても良い //Required("id") }) Response(OK, IntegerType) // 指定したリソースが無ければNotFoundを返す可能生がある Response(NotFound) Response(BadRequest, ErrorMedia) }) }) // Swaggerをローカルで実行するめの定義 var _ = Resource("swagger", func() { Origin("*", func() { Methods("GET") }) Files("/swagger.json", "swagger/swagger.json") Files("/swagger/*filepath", "public/swagger/") })
コードを生成する
インストール方法やコマンドの説明はikawahaさんの記事がわかりやすいので、紹介しておきます。
ikawaha.hateblo.jp
今回はgithub.com/tikasan/goa-stater
のrepoをgo getした想定で説明します。
goagen bootstrap -d github.com/tikasan/goa-stater/design
コマンドを実行すると以下のようなログが流れます。 実行出来ない場合は、designパッケージ(フォルダ)の指定が間違っています。
app/contexts.go app/controllers.go ..... ..... swagger swagger/swagger.json swagger/swagger.yaml
出来上がる構成は以下のような感じです。
実際に作業をするファイルは一部です。
app,client,tool,swagger周りは自動で生成されたもので、変更することはないです。
. ├── LICENSE ├── Makefile ├── README.md ├── actions.go <--------- actionsリソース ├── app ├── client ├── design │ ├── api_definition.go │ ├── media_types.go │ ├── resources.go │ └── security.go ├── main.go <------------ Middlewareとかそこらへんやる ├── public │ └── swagger ├── security.go ├── swagger ├── swagger.go └── tool
このままだと少しごちゃごちゃしているので、私はcontrollerというパッケージにまとめたりしています。
. ├── LICENSE ├── Makefile ├── README.md ├── actions.go <--------- controllerフォルダへ移動させる ├── app ├── client ├── controller ├── design │ ├── api_definition.go │ ├── media_types.go │ ├── resources.go │ └── security.go ├── main.go ├── public │ └── swagger ├── security.go ├── swagger ├── swagger.go <------------ controllerフォルダへ移動させる └── tool
main.goを若干記述を変える必要があるので、調整だけしてください。
//go:generate goagen bootstrap -d github.com/tikasan/goa-stater/design package main import ( "github.com/goadesign/goa" "github.com/goadesign/goa/middleware" "github.com/tikasan/goa-stater/app" "github.com/tikasan/goa-stater/controller" <-------importする ) func main() { // Create service service := goa.New("goa simple sample") // Mount middleware service.Use(middleware.RequestID()) service.Use(middleware.LogRequest(true)) service.Use(middleware.ErrorHandler(service, true)) service.Use(middleware.Recover()) // Mount "actions" controller c := controller.NewActionsController(service) <-----controllerパッケージからメソッドを呼ぶ app.MountActionsController(service, c) // Start service if err := service.ListenAndServe(":8080"); err != nil { service.LogError("startup", "err", err) } }
これで準備完了しました。ここまでの作業は正直テンプレートなので、慣れればほとんど時間がかかりません。
ビジネスロジックを組んでみよう
package controller import ( "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) // ActionsController implements the actions resource. type ActionsController struct { *goa.Controller } // NewActionsController creates a actions controller. func NewActionsController(service *goa.Service) *ActionsController { return &ActionsController{Controller: service.NewController("ActionsController")} } // Action ID func (c *ActionsController) ID(ctx *app.IDActionsContext) error { /* Paramsで定義したものはctx.hogeで受け取れる Params(func() { Param("ID", Integer, "ID") }) */ if ctx.ID == 0 { // Response(NotFound) // 0の場合は、404エラーを返す return ctx.NotFound() } // Response(OK, IntegerType) // MediaType IntegerTypeでレスポンス res := &app.Integer{} /* IDはInteger型で Attribute("ID", Integer, "ID", func() { Example(1) }) */ res.ID = ctx.ID return ctx.OK(res) } // Hello runs the hello action. func (c *ActionsController) Hello(ctx *app.HelloActionsContext) error { // ActionsController_Ping: start_implement // Put your logic here name := ctx.Name // ActionsController_Ping: end_implement res := &app.Message{} res.Message = "Hello " + name return ctx.OK(res) } // Ping runs the ping action. func (c *ActionsController) Ping(ctx *app.PingActionsContext) error { // ActionsController_Ping: start_implement // Put your logic here message := "pong" // ActionsController_Ping: end_implement res := &app.Message{} res.Message = message return ctx.OK(res) }
以上でAPIのビジネスロジックの実装完了です。簡単でしょう?
では、実行してみましょう。
go run main.go
エラーが起きた場合は、importミスやポートが空いてないや何かしらのミスが考えられます。
curl -v http://localhost:8080/api/v1/actions/1 * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/actions/1 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.integer+json < Date: Fri, 05 May 2017 11:49:59 GMT < Content-Length: 9 < {"id":1} * Connection #0 to host localhost left intact
上記のような感じでレスポンスが返ってきました。
試しに間違ったリクエストを投げてみましょう。
curl -v http://localhost:8080/api/v1/actions/hoge * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/actions/hoge HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 400 Bad Request < Content-Type: application/vnd.goa.error < Date: Fri, 05 May 2017 12:01:54 GMT < Content-Length: 188 < {"id":"Iwzpk08B","code":"invalid_request","status":400,"detail":"invalid value \"hoge\" for parameter \"id\", must be a integer","meta":{"expected":"integer","param":"ID","value":"hoge"}} * Connection #0 to host localhost left intact
エラーのフォーマットはカスタム出来ますが、いまはデフォルトで定義されているフォーマットで返ってきます。特にこだわりがなければこれで完了します。
SwaggerUIを使ってみよう
swaggerのドキュメントも出来上がっているので、github.com/tikasan/goa-stater
を使っている場合は、以下のコマンドでAPIのドキュメントの参照とテストが出来ます!!!
open http://localhost:8080/swagger/index.html
初めの方に説明した。ビズネスロジックだけを書けば実装が完了していることが実感出来たと思います。他にもヘッダーのチェックやmodel定義が簡単に出来るgormaなど色々あるので、どんどん紹介していきたいと思います。
goa紹介で使うソース達は、以下のリポジトリに随時更新していきます。
github.com