goaを使ってAPIサーバをサクッと作る
概要
目的
JSONを返すREST APIサーバをサクッと作りたいという要求がちょくちょくあります。
そのたびにサーバの骨組みを作ってリクエストのバリデーションをして…みたいな作業をするのに嫌気がさしたのでJSON APIのクッキーカッターを作ろうと思い立ちました。
目的は上記のとおりなので「こっちのほうが楽なんだがw」といったものがあれば是非教えて下さい!!
なぜgoaを選んだか
まずgoaとはなにかなんですが以下に公式Docを引用します ref. https://github.com/goadesign/goa
Goa takes a different approach to building services by making it possible to describe thedesignof the service API using a simple Go DSL. Goa uses the description to generate specialized service helper code, client code and documentation.
以下が主な選定理由です。
Swaggerが生成されるのが結構いいなと思っています。(普通は逆かもしれませんが) Swaggerを起点にしてJMeterなどの負荷試験ツールのコードも生成したいという野望があります。
Prerequisites
Install goa
goa
コマンドをインストールします。
グローバルを汚したくないという方は適宜moduleを使ってください
$ go get -u goa.design/goa/v3/cmd/goa
作っていく
Design file を書く
プロジェクトを作り、moduleを初期化します。
$ mkdir -p my-project/design $ cd my-project/ $ export GO111MODULE=on $ go mod init # edit design/design.go
後半でDockerも使うのでDocker環境も適宜用意してください。
Design file というインタフェースを定義するファイルを記述します。 今回はリクエストをそのまま返すechoサーバを実装します。 パラメータは
name
: URLのパスとして受け取る (required)age
: クエリストリングで受け取る (required)
です。 POSTでリクエストボディを受け取ったり、デフォルト値を設定したり、フォーマットを指定したりなど便利な機能はたくさんありますが今回は全体の流れを説明するために割愛します。(今後書いていきたい)
DSLの文法自体も詳しくは解説しませんがなんとなく雰囲気で読めると思います。(押し付け)
詳しくは公式のリファレンスを参照してください。v2
とv3
でかなり書き方が違うのでどちらのバージョンかをしっかり確認してください。
dsl - GoDoc
意外とサンプルが転がっていないので(特に日本語サイトはない)公式のExampleも参照するといいと思います。 GitHub - goadesign/examples: Examples for goa showing specific capabilities
design/desing.go
package design import . "goa.design/goa/v3/dsl" // API describes the global properties of the API server. var _ = API("echo", func() { Title("Echo Service") Description("This is HTTP echo service") Server("echo-server", func() { Host("localhost", func() { URI("http://0.0.0.0:8088") }) }) }) // Service describes a service var _ = Service("echo-service", func() { Description("Echo your request") // Method describes a service method (endpoint) Method("echo-get", func() { // define request payload Payload(func() { // Attribute describes an object field Attribute("name", String, "Your name") Attribute("age", Int, "Your age") // Both attributes must be provided when invoking "add" Required("name", "age") }) // define response data type Result(String) // HTTP describes the HTTP transport mapping HTTP(func() { // Requests to the service consist of HTTP GET requests GET("/name/{name}") Param("age") // Responses use a "200 OK" HTTP status // The result is encoded in the response body Response(StatusOK) }) }) })
コードを生成する
Design file をもとにサーバの枠となるファイルを生成します。
# goa gen <MODULE_NAME> $ goa gen github.com/kaito2/my-project/design gen/echo_service/client.go gen/echo_service/endpoints.go gen/echo_service/service.go gen/http/cli/echo_server/cli.go gen/http/echo_service/client/cli.go gen/http/echo_service/client/client.go gen/http/echo_service/client/encode_decode.go gen/http/echo_service/client/paths.go gen/http/echo_service/client/types.go gen/http/echo_service/server/encode_decode.go gen/http/echo_service/server/paths.go gen/http/echo_service/server/server.go gen/http/echo_service/server/types.go gen/http/openapi.json gen/http/openapi.yaml # goa example <MODULE_NAME> $ goa example github.com/kaito2/my-project/design cmd/echo_server-cli/http.go cmd/echo_server-cli/main.go cmd/echo_server/http.go cmd/echo_server/main.go echo_service.go $ tree . . ├── cmd │ ├── echo_server │ │ ├── http.go │ │ └── main.go │ └── echo_server-cli │ ├── http.go │ └── main.go ├── design │ └── design.go ├── echo_service.go ├── gen │ ├── echo_service │ │ ├── client.go │ │ ├── endpoints.go │ │ └── service.go │ └── http │ ├── cli │ │ └── echo_server │ │ └── cli.go │ ├── echo_service │ │ ├── client │ │ │ ├── cli.go │ │ │ ├── client.go │ │ │ ├── encode_decode.go │ │ │ ├── paths.go │ │ │ └── types.go │ │ └── server │ │ ├── encode_decode.go │ │ ├── paths.go │ │ ├── server.go │ │ └── types.go │ ├── openapi.json │ └── openapi.yaml ├── go.mod └── go.sum
ロジックを書く
リクエストを処理するために編集するファイルはプロジェクトルートディレクトリにある echo_server.go
のみです。
echo_server.go
package echo import ( "context" "log" echoservice "github.com/kaito2/my-project/gen/echo_service" ) // echo-service service example implementation. // The example methods log the requests and return zero values. type echoServicesrvc struct { logger *log.Logger } // NewEchoService returns the echo-service service implementation. func NewEchoService(logger *log.Logger) echoservice.Service { return &echoServicesrvc{logger} } // EchoGet implements echo-get. func (s *echoServicesrvc) EchoGet(ctx context.Context, p *echoservice.EchoGetPayload) (res string, err error) { s.logger.Print("echoService.echo-get") return }
gRPCでスタブを生成した事がある人は見慣れたコードかもしれません。
このEchoGet()
関数にロジックを記述します。
以下のように記述することでリクエストのパラメータを取得できます。
... // EchoGet implements echo-get. func (s *echoServicesrvc) EchoGet(ctx context.Context, p *echoservice.EchoGetPayload) (res string, err error) { s.logger.Println("echoService.echo-get") s.logger.Printf("Request name: %s", p.Name) s.logger.Printf("Request age : %d", p.Age) return fmt.Sprintf("Your name: %s, Your age: %d", p.Name, p.Age), nil } ...
実行する
実際にサーバを起動してみます。
$ go build ./cmd/echo_server $ ./echo_server [echo] 21:19:42 HTTP "EchoGet" mounted on GET /add/{name} [echo] 21:19:42 HTTP server listening on "localhost:8088"
別のターミナルからアクセスすると…
$ curl "http://localhost:8088/name/kaito2?age=13" "Your name: kaito2, Your age: 13"
無事動作しました。
ちなみに Required
に指定したパラメータを記述しないと400
が返ります。
$ curl "http://localhost:8088/add/kaito2" {"name":"missing_field","id":"XXXXXXX","message":"\"age\" is missing from query string; invalid value \"\" for \"age\", must be a integer","temporary":false,"timeout":false,"fault":false}
上で書いたクライアントも生成されているので利用します。
$ go build ./cmd/echo_server-cli/ $ ./echo_server-cli --help ./echo_server-cli is a command line client for the echo API. Usage: ./echo_server-cli [-host HOST][-url URL][-timeout SECONDS][-verbose|-v] SERVICE ENDPOINT [flags] -host HOST: server host (localhost). valid values: localhost -url URL: specify service URL overriding host URL (http://localhost:8080) -timeout: maximum number of seconds to wait for response (30) -verbose|-v: print request and response details (false) Commands: echo-service echo-get Additional help: ./echo_server-cli SERVICE [ENDPOINT] --help Example: ./echo_server-cli echo-service echo-get --name "Quam dolores." --age 8576636692946696796 $ ./echo_server-cli echo-service echo-get --name "Quam dolores." --age 8576636692946696796 "Your name: Quam dolores., Your age: 8576636692946696796"
デフォルトでsampleの値が用意されているので変な値になっていますが(笑)
クライントで動作確認もできます(curl
でいい気もするが)
Dockerizeする
せっかく(?)なのでDockerizeしましょう。
Dockerfile
# Dockerfile References: https://docs.docker.com/engine/reference/builder/ FROM golang:1.11.12 as builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/echo_server FROM alpine:3.10.1 WORKDIR /app EXPOSE 8080 COPY --from=builder /app/main . CMD ["./main"]
プロダクションで alpine ってどうなんだろう…結構使われてるのかな…
実行してみます。
$ docker login $ docker build . -t my-project-image $ docker run --rm -p 8088:8088 my-project-image [echo] 12:48:25 HTTP "EchoGet" mounted on GET /name/{name} [echo] 12:48:25 HTTP server listening on "0.0.0.0:8088"
いい感じに実行できました!!
まとめ
という流れで簡単にサーバができました!! これをCIでビルドしてCloud Runにデプロイできるようにして…と夢が広がります!!
DSLに関しては少し癖があり、色々動作検証をしてみたのでまた記事にしたいと思います。