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

goa の API デザインの書き方 前編 (API と MediaType)

golang Go言語 goadesign

はじめに

goa の API デザインについて,デザインを定義する4つの要素について概要を説明します.

  • APIAPI サーバの定義
  • MediaType … レスポンスデータの定義
  • Resource … APIが管理するデータへのアクセス方法 / エンドポイントなどを定義
  • Payload … API に送信するデータの定義

とりあえずこれらを押さえておけば一通りのAPIは書けるはず!(たぶん)

今回は4つのうちの API と MediaType を説明します.


準備:API サンプル

おなじみの最小構成サンプル.

デザインのパッケージ名は design.あと,goa のライブラリを dot インポートしてますが,これはそういう流儀なので呪文だと思って許して下さい.以下に出てくる API とか Resource とか MediaType といった関数は,これらのライブラリの中で定義されている関数です.

これらの関数の説明は goa :: Design-first API Generation に説明がありますので,こちらを参照しながら読んでいただけるといいかと思います.

このような関数を組み上げて API のデザインを書いていくスタイルが goa のスタイルになります.

package design                                     // The convention consists of naming the design
                                                   // package "design"
import (
        . "github.com/goadesign/goa/design"        // Use . imports to enable the DSL
        . "github.com/goadesign/goa/design/apidsl"
)

var _ = API("cellar", func() {                     // API defines the microservice endpoint and
        Title("The virtual wine cellar")           // other global properties. There should be one
        Description("A simple goa service")        // and exactly one API definition appearing in
        Scheme("http")                             // the design.
        Host("localhost:8080")
})

var _ = Resource("bottle", func() {                // Resources group related API endpoints
        BasePath("/bottles")                       // together. They map to REST resources for REST
        DefaultMedia(BottleMedia)                  // services.

        Action("show", func() {                    // Actions define a single API endpoint together
                Description("Get bottle by id")    // with its path, parameters (both path
                Routing(GET("/:bottleID"))         // parameters and querystring values) and payload
                Params(func() {                    // (shape of the request body).
                        Param("bottleID", Integer, "Bottle ID")
                })
                Response(OK)                       // Responses define the shape and status code
                Response(NotFound)                 // of HTTP responses.
        })
})

// BottleMedia defines the media type used to render bottles.
var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
        Description("A bottle of wine")
        Attributes(func() {                         // Attributes define the media type shape.
                Attribute("id", Integer, "Unique bottle ID")
                Attribute("href", String, "API href for making requests on the bottle")
                Attribute("name", String, "Name of wine")
                Required("id", "href", "name")
        })
        View("default", func() {                    // View defines a rendering of the media type.
                Attribute("id")                     // Media types may have multiple views and must
                Attribute("href")                   // have a "default" view.
                Attribute("name")
        })
})

では本題.

API定義

func API

var _ = API("cellar", func() {                     // API defines the microservice endpoint and
        Title("The virtual wine cellar")           // other global properties. There should be one
        Description("A simple goa service")        // and exactly one API definition appearing in
        Scheme("http")                             // the design.
        Host("localhost:8080")
})

API全体の定義です.これは見てもらえば何となく分かると思います.関数API のようにデザインのトップレベルに書くDSL関数のことを,goa ではトップレベAPI DSL と呼んでいます.トップレベAPI は,下記の4つがあります.これらはこれから順に説明していきますが,関数Typeは Payload の説明の時に解説します.

トップレベAPI DSL

関数 API の返値は _ で捨てられています.慣れるまで気持ち悪いですけど,こういうものだと思って下さい.

API で定義されている要素の説明

要素 説明
Title APIのタイトル.ドキュメントなどで表示される
Description このAPIの詳しい説明.ドキュメントなどで表示される
Scheme "http"や"https"などの URL scheme をセットできる
Host サービスするホスト名とポート

この他にも,関数 VersionLicenceDocs なんてのも指定可能です.また,セキュリティの設定が必要な場合はここにセキュリティ設定用の要素が入ることもあります. 詳細はドキュメントを参照して下さい.細かいところはまた別の機会に説明したいと思います.

MediaType定義

→ func MediaType

// BottleMedia defines the media type used to render bottles.
var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
        Description("A bottle of wine")
        Attributes(func() {                         
                Attribute("id", Integer, "Unique bottle ID")  // id は整数型
                Attribute("href", String, "API href for making requests on the bottle") // href は文字列
                Attribute("name", String, "Name of wine") // name は文字列

                Required("id", "href", "name") // 上記のうちで必須なものをここに指定する
        })
        View("default", func() {                // default View は必須
                Attribute("id")                     
                Attribute("href")                   
                Attribute("name")
        })
})

関数API と違って,関数MediaType の返値は変数に保存されています.これは,この変数を使って,別の場所(定義)でこのメディアタイプを利用できるようにするためです.

メディアタイプは,レスポンスデータの形式を定義します.メディアタイプに名前をつけて(ここでは "application/vnd.goa.example.bottle+json".これは自分で適当に定義します),関数Attributes でレスポンスに含まれるデータをすべて列挙します.この例では,id / href / name が指定されています.また,それぞれに型が定義できます.idIntegerhrefnameString で定義されています.

型として利用できる基本的なものは,Integer / Number / String / Boolean などがあります.Number浮動小数点になります.JSON データと対応してもらうと理解しやすいと思います.また,配列を表すための関数 ArrayOfや,Hash を表すための HashOf があります.例えば,ArrayOf(Integer) とすれば JSON[1,2,3,4]のような配列,HashOf(String, Integer) とすれば JSON{"orange":1, "apple":3} のようなデータ形式が表現できます.

goaの基本型 golangでの表現 JSONでの表現
Integer int number
Number float number
String string string
Boolean bool boolean
DateTime time.Time RFC3339な文字列
UUID uuid.UUID RFC4122な文字列
Any interface{} ---

ここで注意したいのは,関数Attributes で定義したのは,レスポンスデータに現れうるデータ要素であって,実際のレスポンス形式ではないということです. 実際のレスポンス形式は 関数ViewAttributes で定義したデータ要素を組み合わせて作ります. たとえば,上の例ではレスポンスは "default" View で定義されていて,

{id: 1, href: "/bottles/1", "name" : "Bottle #1"}

と返ります.関数Required はデータ要素の値がゼロ値でも,要素として必須かどうかを示しています.たとえば,name を必須要素から外した場合,返却される name の値が空文字列の場合には

{id: 1, href: "/bottles/1"}

のように name が省略されたレスポンスが返されます.ですが,まぁ,省略されちゃうと分かりにくいので,メディアタイプの定義で,Attribute を必須要素から外すことはあんまりないかなという気がします.

やるとしたら,実は同じメディアタイプに対して異なる View をいくつか定義することも可能なので,詳細なレスポンスを返したいときと,省略したレスポンスを返したいときで View をそれぞれ用意しておくという風にする方がいいかもしれません.

説明したように,View はいくつか定義可能ですが,"default" View は必須なので必ず定義して下さい.

余談:メディアタイプはどうやって決めるか?

慣習的に "application/vnd.<リソースの名前>+<形式>" で決めるようです.vnd というのはベンダー定義のメディアタイプであることを示しています.goa は特に何も指定しなければレスポンスは JSON 形式になるので,形式の部分は "+json" が指定されています.この辺の説明は Web API: The Good Parts の 4.4節に分かりやすくまとまっているのを参考にさせてもらいました.Web API: The Good Parts いい本なので是非.

Web API: The Good Parts

Web API: The Good Parts

参考