読者です 読者をやめる 読者になる 読者になる

ぺい

大阪の専門学生の落書き。主にエンジニア寄りの話。

goのgoaでAPIをデザインしよう(レスポンス編)

goaはいいぞ!

f:id:tikasan0804:20170505212036p:plain

  1. goのgoaでAPIをデザインしよう(基本編) - ぺい
  2. goのgoaでAPIをデザインしよう(バリデーション編) - ぺい
  3. goのgoaでAPIをデザインしよう(レスポンス編) - ぺい
  4. goのgoaでAPIをデザインしよう(クライアント編) - ぺい
  5. goのgoaでAPIをデザインしよう(エンドポイント編) - ぺい

レスポンス

いきなりですが、以下のエンドポイントを見てください。RESTを知ってる人は、レスポンスが違うことが分かると思います。

GET /users/1
GET /users

開発するチームにもよって変わる可能生はあると思いますが、GET /users/1は恐らくひとりのユーザー情報だけを返す。そして、GET /usersはひとり以上のユーザー情報を返す。
また、GET /usersはIDや名前だけで良かったり、GET /users/1は詳細な情報が欲しかったり、同じリソースに対しての操作でも、レスポンスは変える必要があります。(と思っています)

具体的にレスポンスの例を示すと以下のような感じになります。

GET /users

[
  {
    "id": 1,
    "name": "ユーザー1"
  },
  {
    "ID": 2,
    "name": "ユーザー2"
  }
]

GET /users/1

{
  "id": 1,
  "email": "satak47cpc@gmail.com",
  "name": "ユーザー1"
}

また、場合によってはIDの一覧だけ欲しい時もあったり

{"america":3,"japan":2,"korea":4}

キーに意味を持たせた連想配列が欲しかったり

[1,2,3]

上記のような需要全てに対応することが出来ます。実際に動かしてみましょう。

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("response", func() {
    BasePath("/response")
    Action("list", func() {
        Description("ユーザー(複数)")
        Routing(
            GET("/users"),
        )
        // 複数返す
        Response(OK, CollectionOf(UserMedia))
        Response(BadRequest, ErrorMedia)
    })
    Action("show", func() {
        Description("ユーザー(単数)")
        Routing(
            GET("/users/:id"),
        )
        // 単一
        Response(OK, UserMedia)
        Response(BadRequest, ErrorMedia)
    })
    Action("hash", func() {
        Description("ユーザー(ハッシュ)")
        Routing(
            GET("/users/hash"),
        )
        // 連想配列
        Response(OK, HashOf(String, Integer))
        Response(BadRequest, ErrorMedia)
    })
    Action("array", func() {
        Description("ユーザー(配列)")
        Routing(
            GET("/users/array"),
        )
        // 配列
        Response(OK, ArrayOf(Integer))
        Response(BadRequest, ErrorMedia)
    })
})

MediaTypeを定義する

package design

import (
    . "github.com/goadesign/goa/design"
    . "github.com/goadesign/goa/design/apidsl"
)

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")
    })
})

Controllerを定義する

goagen bootstrap -d github.com/tikasan/goa-stater/design

特にビジネスロジックなどはなしで、値を返すだけのコードを実装します。

package controller

import (
    "github.com/goadesign/goa"
    "github.com/tikasan/goa-stater/app"
)

// ResponseController implements the response resource.
type ResponseController struct {
    *goa.Controller
}

// NewResponseController creates a response controller.
func NewResponseController(service *goa.Service) *ResponseController {
    return &ResponseController{Controller: service.NewController("ResponseController")}
}

// Array runs the array action.
func (c *ResponseController) Array(ctx *app.ArrayResponseContext) error {
    // ResponseController_Array: start_implement

    // Put your logic here

    // ResponseController_Array: end_implement
    res := make([]int, 3)
    res[0] = 1
    res[1] = 2
    res[2] = 3
    return ctx.OK(res)
}

// Hash runs the hash action.
func (c *ResponseController) Hash(ctx *app.HashResponseContext) error {
    // ResponseController_Hash: start_implement

    // Put your logic here

    // ResponseController_Hash: end_implement
    res := make(map[string]int, 3)
    res["japan"] = 2
    res["america"] = 3
    res["korea"] = 4
    return ctx.OK(res)
}

// List runs the list action.
func (c *ResponseController) List(ctx *app.ListResponseContext) error {
    // ResponseController_List: start_implement

    // Put your logic here

    // ResponseController_List: end_implement
    res := make(app.UserTinyCollection, 2)
    u1 := &app.UserTiny{
        ID:   1,
        Name: "ユーザー1",
    }
    u2 := &app.UserTiny{
        ID:   2,
        Name: "ユーザー2",
    }
    res[0] = u1
    res[1] = u2
    return ctx.OKTiny(res)
}

// Show runs the show action.
func (c *ResponseController) Show(ctx *app.ShowResponseContext) error {
    // ResponseController_Show: start_implement

    // Put your logic here

    // ResponseController_Show: end_implement
    res := &app.User{}
    res.ID = 1
    res.Name = "ユーザー1"
    res.Email = "satak47cpc@gmail.com"
    return ctx.OK(res)
}

準備が整ったので、実行してリクエストを投げてみましょう!

$ go run main.go
$ curl -v 'http://localhost:8080/api/v1/response/users/1'
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /api/v1/response/users/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: application/vnd.user+json
< Date: Sat, 06 May 2017 04:33:08 GMT
< Content-Length: 63
< 
{"ID":1,"email":"satak47cpc@gmail.com","name":"ユーザー1"}
* Connection #0 to host localhost left intact
$ curl -v 'http://localhost:8080/api/v1/response/users'
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /api/v1/response/users HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: application/vnd.user+json; type=collection
< Date: Sat, 06 May 2017 04:35:02 GMT
< Content-Length: 66
< 
[{"ID":1,"name":"ユーザー1"},{"ID":2,"name":"ユーザー2"}]
* Connection #0 to host localhost left intact

いい感じのデータが返ってきました。goaはいいぞ!