GolangのgoaでAPIをデザインしよう(エンドポイント編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
エンドポイントにも色々ある
同じエンドポイントの指定でも、DSLの書き方で変わります。若干癖があったりするので、今回はそれについてまとめたいと思います。
例:ユーザーのフォロー操作。
同じ名前だけど、HTTPメソッドが違うので、操作の中身が違う。
PUT /users/follow // フォローする DELETE /users/follow // フォローを外す
例:なんかのリスト
/listというリソースに対してエンドポイントで操作を表現している。(途中まで操作同じだけど、若干違う)
GET /list/new // 新しいリスト GET /list/topic // 注目されてるリスト GET /list // リスト(全件)
例:ちょっと特殊ケース
GET /api/v1/method/users/:id/follow/:type
以上のエンドポイントをDSLでどうやって表現するかを紹介します。
goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。
ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。
$ go get github.com/tikasan/goa-stater または $ ghq get github.com/tikasan/goa-stater
APIをデザインする
順を追って記事を読んでいればすんなりソースも読めると思います。(つまりコメントがなし)
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = Resource("method", func() { BasePath("/method") Action("method", func() { Description("HTTPメソッド") Routing( GET("/get"), POST("/post"), DELETE("/delete"), PUT("/put"), ) Response(OK, MessageMedia) Response(BadRequest, ErrorMedia) }) Action("list", func() { Description("リストを返す") Routing( GET("/list"), GET("/list/new"), GET("/list/topic"), ) Response(OK, CollectionOf(UserMedia)) Response(BadRequest, ErrorMedia) }) Action("follow", func() { Description("フォロー操作") Routing( PUT("/users/follow"), DELETE("/users/follow"), ) Response(OK, MessageMedia) Response(BadRequest, ErrorMedia) }) Action("etc", func() { Routing(GET("/users/:id/follow/:type")) Description("ちょっと特殊ケース") Params(func() { Param("id", Integer, "id") Param("type", Integer, "タイプ", func() { Enum(1, 2, 3) }) }) Response(OK, "plain/text") Response(BadRequest, ErrorMedia) }) })
MediaTypeを定義する
以前記事で書いたコードです。
var UserMedia = MediaType("application/vnd.user+json", func() { Description("example") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "名前", func() { Example("hoge") }) Attribute("email", String, "メールアドレス", func() { Example("satak47cpc@gmail.com") }) Required("id", "name", "email") }) // 特別な指定がない場合はdefaultのMediaType View("default", func() { Attribute("id") Attribute("name") Attribute("email") }) // tinyという名前の場合は、簡潔なレスポンスフォーマットにすることが出来る View("tiny", func() { Attribute("id") Attribute("name") }) })
コントローラー定義
デザインを元にコードを生成しましょう。コントローラーは、値を返すだけの単純なものを作成します。
$ goagen bootstrap -d github.com/tikasan/goa-stater/design
package controller import ( "fmt" "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) // MethodController implements the method resource. type MethodController struct { *goa.Controller } // NewMethodController creates a method controller. func NewMethodController(service *goa.Service) *MethodController { return &MethodController{Controller: service.NewController("MethodController")} } // Etc runs the etc action. func (c *MethodController) Etc(ctx *app.EtcMethodContext) error { // MethodController_Etc: start_implement // Put your logic here // MethodController_Etc: end_implement return ctx.OK([]byte(fmt.Sprintf("ID: %d, Type %d", ctx.ID, ctx.Type))) } // Follow runs the follow action. func (c *MethodController) Follow(ctx *app.FollowMethodContext) error { // MethodController_Follow: start_implement // Put your logic here var message string // 何かのフォロー操作 if "PUT" == ctx.Request.Method { message = "フォローした" } else if "DELETE" == ctx.Request.Method { message = "フォロー外した" } // MethodController_Follow: end_implement res := &app.Message{} res.Message = message return ctx.OK(res) } // List runs the list action. func (c *MethodController) List(ctx *app.ListMethodContext) error { // MethodController_List: start_implement // Put your logic here var listType string switch ctx.RequestURI { //case client.ListMethodPath(): <---これでもいけるけど・・・って感じ 何かいい方法。 case "/api/v1/method/list": listType = "ただの" case "/api/v1/method/list/new": listType = "新しい" case "/api/v1/method/list/topic": listType = "注目されている" } // MethodController_List: end_implement res := make(app.UserTinyCollection, 2) u1 := &app.UserTiny{ ID: 1, Name: listType + "ユーザー1", } u2 := &app.UserTiny{ ID: 2, Name: listType + "ユーザー2", } res[0] = u1 res[1] = u2 return ctx.OKTiny(res) } // Method runs the method action. func (c *MethodController) Method(ctx *app.MethodMethodContext) error { // MethodController_Method: start_implement // Put your logic here message := ctx.RequestURI // MethodController_Method: end_implement res := &app.Message{} res.Message = message return ctx.OK(res) }
実際に動かしてみよう
$ go run main.go
$ curl -v -X PUT 'http://localhost:8080/api/v1/method/users/follow' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > PUT /api/v1/method/users/follow HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.message+json < Date: Sat, 06 May 2017 06:34:09 GMT < Content-Length: 33 < {"message":"フォローした"} * Connection #0 to host localhost left intact
$ curl -v -X DELETE 'http://localhost:8080/api/v1/method/users/follow' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > DELETE /api/v1/method/users/follow HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.message+json < Date: Sat, 06 May 2017 06:35:05 GMT < Content-Length: 36 < {"message":"フォロー外した"} * Connection #0 to host localhost left intact
$ curl -v 'http://localhost:8080/api/v1/method/users/1/follow/3' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/method/users/1/follow/3 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: plain/text < Date: Sat, 06 May 2017 06:35:35 GMT < Content-Length: 13 < * Connection #0 to host localhost left intact ID: 1, Type 3%
これで出来上がりです。エンドポイントのURIによって処理を変えるところが少し不細工なんで、誰か良い案あれば教えて欲しいです。
goa紹介で使うソース達は、以下のリポジトリに随時更新していきます。
github.com