BeegoでJSONを返すAPIサーバを立てる

Go言語のWebフレームワークであるBeegoを始めました。

公式 https://beego.me/
GitHub https://github.com/astaxie/beego

Go言語のフレームワークの中では、パフォーマンス的にとくに飛び抜けて良い悪いはなさそうです。
https://github.com/smallnest/go-web-framework-benchmark

Beegoには普通のWebページを作成するプロジェクト生成以外に、APIサーバを作成するためのプロジェクト生成機能がついています。
この機能を使ってレスポンスをJSONで返すAPIサーバを立ててみたメモです。

# beeコマンドのインストール

Beegoはbeeコマンドを使ってプロジェクトやファイルの操作を行います。

普段あんまりこういったコマンドによる生成は使わないよ派なんですが、beeコマンドによって自動生成されたファイルにはSwagger用のコメントがぎっしり書いてあったので、Beegoに関しては積極的に使っていこうと思います。

なお、$GOPATH/binにパスが通ってないとコマンドが使えないので注意。
デフォルトは~/go/がGOPATHなので、~/go/binにパスが通っていればOK。

```bash
$ go get -u github.com/beego/bee
$ bee version
```

次のようなVersion画面が表示されます

```
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.10.0

├── Beego     : Beego is not installed. Please do consider installing it first: https://github.com/astaxie/beego
├── GoVersion : go1.12.9
├── GOOS      : linux
├── GOARCH    : amd64
├── NumCPU    : 8
├── GOPATH    : /home/?????/go
├── GOROOT    : /usr/local/go
├── Compiler  : gc
└── Date      : Friday, 30 Aug 2019
```

# プロジェクトの作成

今後を見据えて go modules を使っていきます。なお俺氏、まだ使い方がよく分かっていないもよう\_(:3」∠)_

まずはプロジェクト用のディレクトリを作成しGOPATHを設定します。

```bash
$ mkdir test && cd test
$ export GOPATH=$(pwd)
```

GOPATHの中にsrcディレクトリを作り、その中でbee apiコマンドでAPI用プロジェクトを作成します。

```bash
$ mkdir src && cd src
$ bee api hello
$ cd hello
```
モジュール対応を行ってからbuildします。

```bash
$ export GO111MODULE=on
$ go mod init hello
$ go build
```
go modulesを使ったので、github.com/astaxie/beegoが自動でgo getされます。

最初のビルドが終わったらサーバを起動します。

```bash
$ bee run
2019/08/30 23:29:16.528 [I] [asm_amd64.s:1337]  http server Running on http://:8080
```
localhostの8080ポートで待受が始まったことが通知されますが、まだ接続せずに一旦Ctrl+Cで終了します。
http://localhost:8080/ に接続しても何もありません。Not Foundが表示されるだけなので慌てない、慌てない。

※なお、bee runはhotreload対応なので、プログラムを書き換えたら自動でサーバが更新されます

# Swaggerを試す

公式 https://beego.me/blog/beego_api

どのようなAPIがあるかを確認するには、Swagger-UIを見るのが手っ取り早いです。Swatterドキュメント生成付きで生成するには、bee runにオプションを追加して実行します。

```bash
$ bee run -downdoc=true -gendoc=true
```

これで、http://localhost:8080/swagger/ にアクセスしたらSwagger-UIが表示されるはずです。プロジェクト作成時に自動で生成されたサンプルのAPIが見えています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/6287ef7e-3e05-a466-3f93-f5c3890cf6d1.png)

試しにGETのAPIを実行してみます。GETのボタンを押して、Try it outボタンを押すと、Executeボタンが表示されるのでコレを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/0262592f-b67d-388a-22cc-386f99413004.png)

下の方にStatus CodeとResponse Bodyが表示されますが、見てほしいのはCurlのところです。Curlの接続ホストが http://localhost:8080/v1/object/ と、v1が挟まっていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/2de1573a-85e5-3201-b106-ae2b04700169.png)

bee apiでプロジェクトを作ると、v1というnamespaceにAPIがまとめられています。

# エラーを起こしてみる

では、v1を無視して http://localhost:8080/user/ にアクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/d502b2dc-23ca-b402-a770-e408108e0de3.png)
おなじみ404エラーの画面です。はい、404エラー画面が表示されます。
ええ、404エラーが「画面(HTML)」で表示されます。えぇ……JSONじゃないんだ。

次に、ログインAPIを試してみます。ログインに失敗したら認証エラーが起こるはずです。loginはuserの下にあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/5e56779c-a6d2-025b-5a0a-c15868cb6f2b.png)

models/user.go に次のテスト用ユーザーが作られています。

```go:models/user.go
	u := User{"user_11111", "astaxie", "11111", Profile{"male", 20, "Singapore", "astaxie@gmail.com"}}
```
usernameにastaxie、passwordに11111を入れればログインが成功します。
ここでは、わざと失敗するよう変な値をいれてExecuteしてみましょう。レスポンスがこちらです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/5abfaebf-6522-0a29-819d-98a46bdbb159.png)
Status Codeが200で返ってきました。えぇ…

その下に、Responseが403で返ってくると書いてあるにも関わらず、200で返ってきます。えぇ…

っていうか、403？ え、403？ 401じゃないの？

というわけで「サンプルはサンプル、そのまま使うのは絶対にやめてね」という強い意志を感じたので、じゃぁStatu Codeは401で、Response BodyはJSONで返すようにしていこう、というわけです。

# 404がJSONを返すようにする

公式 https://beego.me/docs/mvc/controller/errors.md?fbclid=IwAR0s1Bf8Ql15aWjpaAhrKthsSaDSEv2sFISx9cP3MWNoG5hKJclQVJztMJM#error-handling

次のように、エラー番号とそれを処理する関数を書いて登録する、という方法もあります。

```bash
beego.ErrorHandler("404", 処理する関数)
```
が、１つずつ登録とか面倒だしエラー処理をまとめたい場合が多いと思うので、エラーハンドリング用Controllerを用意する方法を使います。

まず、controllers/の中にerror.goとでも名前をつけたファイルを用意します。最低限の中身はbeego.Controllerを内包したErrorControllerがある状態です。そこに今回は404エラー対応の関数を追加します。

Status Codeに対応するエラーハンドリング関数は、Error数字()という関数になり、ここではError404()という関数を作成しています。
レスポンスにControllerのServeJSON()を使うことでContent-typeをapplication/jsonで確定できます。出力する内容は、ControllerのData["json"]に設定しておきます。

```go:controllers/error.go
package controllers

import (
	"github.com/astaxie/beego"
)

// ErrorController ...
type ErrorController struct {
	beego.Controller
}

// Error404 ...
func (c *ErrorController) Error404() {
	c.Data["json"] = map[string]string{"message": "page not found"}
	c.ServeJSON()
}
```

main()の中でRunする前にErrorControllerを追加すると有効になります。

```go:main.go
func main() {
	if beego.BConfig.RunMode == "dev" {
		beego.BConfig.WebConfig.DirectoryIndex = true
		beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
	}

	// 追加
	beego.ErrorController(&controllers.ErrorController{})

	beego.Run()
}
```
試しにcurlでアクセスすると、次のようなレスポンスが返ってきます。

```bash
$ curl -i http://localhost:8080/
HTTP/1.1 404 Not Found
Content-Length: 33
Content-Type: application/json; charset=utf-8
Server: beegoServer:1.12.0
Date: Fri, 30 Aug 2019 15:25:16 GMT

{
  "message": "page not found"
}
```

# login失敗が401(とJSON)を返すようにする

controllers/error.goにError401を追加しておきます。

```go:controllers/error.go
// Error401 ...
func (c *ErrorController) Error401() {
	c.Data["json"] = map[string]string{"message": "auth failed"}
	c.ServeJSON()
}
```

login失敗時に200が返ってきていたのをなんとかしてみます。問題のコードはcontrollrs/user.goの中にあり、認証に成功しようがしまいがu.ServeJSON()をしているだけでした。

```go:controllrs/user.go
// @Failure 403 user not exist
// @router /login [get]
func (u *UserController) Login() {
	username := u.GetString("username")
	password := u.GetString("password")
	if models.Login(username, password) {
		u.Data["json"] = "login success"
	} else {
		u.Data["json"] = "user not exist"
	}
	u.ServeJSON()
}
```

まず、Swaggerドキュメントが403を返すかのように書かれてた原因となっている Failure 403 を Failure 401 に変更します。そして、model.Login失敗時にu.Data["json"] = "user not exist"しているだけの行の後に、ちゃんと失敗したということをAbortを使用して知らせます。

```go:controllrs/user.go
// @Failure 401 auth failed  // <- ココ変更
// @router /login [get]
func (u *UserController) Login() {
	username := u.GetString("username")
	password := u.GetString("password")
	if models.Login(username, password) {
		u.Data["json"] = "login success"
	} else {
		u.Data["json"] = "user not exist"
		u.Abort("401")  // <- ココ変更
	}
	u.ServeJSON()
}
```
※Abortの中でpanicが呼ばれているので、関数の続きは実行されず即座に終了されます

curlでわざとpasswordを間違えてアクセスすると、401が返ってくることが確認できます。レスポンスもapplication/jsonでauth failedです。

```bash
$ curl -X GET "http://localhost:8080/v1/user/login?usernme=astaxie&password=22222" -H  "accept: application/json" -i
HTTP/1.1 401 Unauthorized
Content-Length: 30
Content-Type: application/json; charset=utf-8
Server: beegoServer:1.12.0
Date: Fri, 30 Aug 2019 15:42:50 GMT

{
  "message": "auth failed"
}
```

Swaggerの方も修正されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/0c58b57e-31d7-45cc-42ba-9a8351261120.png)

ちなみに、Login()のAbortの前とError401()の中でu.Data["json"]が設定されていますが、どちらかで大丈夫です。固定値ならError???()の中で、メッセージを変えたいならAbort()の直前で、という使い分けになると思います。

# 数字以外のエラー

Abortでは数字以外に好きな名前をつけてエラーを発生させることができます。たとえば先程のAbort("401")をAbort("Auth")にしてみます。

```go:controllrs/user.go
func (u *UserController) Login() {
	username := u.GetString("username")
	password := u.GetString("password")
	if models.Login(username, password) {
		u.Data["json"] = "login success"
	} else {
		u.Data["json"] = "user not exist"
		u.Abort("Auth")  // <- ココ変更
	}
	u.ServeJSON()
}
```

error.goの中では、ErrorAuth()という名前の関数を用意することになります。この時、数字以外のエラーはStatus Codeが全て200となりますので、ControllerのCtx.Output.SetStatus()でステータスコードを設定する必要があります。ここでは500エラーを返してみます。

```go:controllers/error.go
// ErrorAuth ...
func (c *ErrorController) ErrorAuth() {  // <- Error+Abortで設定した名前
	c.Ctx.Output.SetStatus(500)  // <- コレが必要
	c.ServeJSON()
}
```

Status Codeが500で返ってきました。ErrorAuthでu.Dataに値を設定しなかったので、Login()の中で設定したuser not existがレスポンスされています。

```bash
$ curl -X GET "http://localhost:8080/v1/usme=astaxie&password=22222" -H  "accept: application/json" -i
HTTP/1.1 500 Internal Server Error
Content-Length: 16
Content-Type: application/json; charset=utf-8
Server: beegoServer:1.12.0
Date: Fri, 30 Aug 2019 15:57:43 GMT

"user not exist"
```

# JSONチェックはcurl(+jq)とFireFoxが便利

curlが便利なのは言わずもがなですね。-i や -I オプションでヘッダが表示されるので、ちゃんとapplication/jsonになってるか確認できます。text/plainとかだったらm9(^Д^)
curlとjqを組み合わせたら、だいたいなんとでもなる！

Content-typeがapplication/jsonの場合は、Firefoxだと内蔵のJSONビューワーが起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/7971bb0b-b4cd-5fbb-682c-f7300c6d9296.png)
これも便利！