前回、Web API を起動してアクセスするところまでを実施しました。(前回はこちら)

前回のコードを使って、今回はいくつか機能を実装していきます。

RESTfulなAPI

Representational State Transfer (REST) は、APIの定義に使用されるアーキテクチャスタイルであり、同時にウェブのような分散ハイパーメディアシステムのためのソフトウェアアーキテクチャのスタイルのひとつでもある。

Representational State Transfer

RESTful な API として実装していきます。

今回扱うリソースはタスク情報とします。タスク情報は以下と定義しておきます。(Json フォーマット)

{
  "content": "hoge",
  "done": false
}

Web API として、以下を実装していきます。

  • GET: タスク情報を取得する
  • POST: タスク情報を登録する
  • PUT: タスク情報を更新する
  • DELETE: タスク情報を削除する

Gin ルーティング

実装していく前に、Web API に必要な Gin Web Framework の機能を見ていきます。

まずは、Gin Web Framework のルーティングです。

Using GET POST PUT PATCH DELETE and OPTIONS に説明があります。

gin.Default() より取得したインスタンスに、 Http Method に対応した関数が定義されているので、関数にパスと処理を指定するようになっています。

以下に例も記載しておきます。

r := gin.Default()
// GET /ping で呼ばれる例
r.GET("/ping", func(c *gin.Context) {
	// 処理を記載
})
// POST /ping で呼ばれる例
r.POST("/ping", func(c *gin.Context) {
	// 処理を記載
})
// PUT /ping で呼ばれる例
r.PUT("/ping", func(c *gin.Context) {
	// 処理を記載
})
// DELETE /ping で呼ばれる例
r.DELETE("/ping", func(c *gin.Context) {
	// 処理を記載
})

Gin データを受け取る/返却する

次に、リクエストの値の受け取り方について確認します。

Gin Web Framework に、Query 文字列やリクエスト Body の値を受け取る方法が記載されています。

例えば URL のパスの値を取得する場合は以下のようになります。

r.GET("/task/:id", func(c *gin.Context) {
	id := c.Param("id")
	
})

パスの定義に :id と指定した場合は、 gin.Context にある Param より値が取得できるようになっています。

例えば /task/1 でアクセスした場合は id に 1 が設定されます。

また、 Query 文字列や RequestBody の値を struct に bind し validation する方法なども提供されています。

Model binding and validation に記載されています。

Must bind 系と Should bind 系が用意されています。 Must bind は、 binding エラーをクライアントに返却するところまでをサポートしています。 Should bind は、 binding エラー時のハンドリングを自分で実装できます。

実装方法は、 Go 言語の StructTag を各 field に設定します。 Query パラメータ等で指定する名前と struct の field をマッピングするには form タグを指定していきます。 確認するため Task struct を用意し form タグを記載して bind の動きを確認します。

type Task struct {
	Content string `form:"content"`
	Done    bool   `form:"done"`
}

func main() {

	// GET /ping の下に追記する

	r.GET("/task", func(c *gin.Context) {
		var task Task
		// Query 文字列を Struct にマッピングする
		_ = c.BindQuery(&task)
		// User struct をJsonで返却する
		c.JSON(http.StatusOK, task)
	})

	// 〜
}

コードを上記のように修正し API を起動後リクエストします。

$ curl -s -XGET "http://localhost:8080/task?content=hoge&done=false"
{"Content":"hoge","Done":false}

レスポンスをみると Query パラメータに指定した値が設定できている事がわかります。

バリデーションを追加します。バリデーションを追加するには binding タグを指定します。 content を必須パラメータとします。 また、BindQuery の戻り値で、バリデーションエラーが発生しているか判断できます。

type Task struct {
	Content string `form:"content" binding:"required"`
	Done    bool   `form:"done"`
}

// 上記 GET /task の中を変更する
r.GET("/task", func(c *gin.Context) {
	var task Task
	// errでない場合のみJsonを返却するようにする
	if err := c.BindQuery(&task); err != nil {
		// エラーを標準出力に表示
		fmt.Println(err)
	} else {
		c.JSON(http.StatusOK, task)
	}
})

API を再起動後リクエストします。

$ curl -vs -XGET "http://localhost:8080/task?content=hoge&done=false"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /user?name=hoge&age=20 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: application/json; charset=utf-8
< Date: 〜
< Content-Length: 〜
<
* Connection #0 to host localhost left intact
{"Content":"hoge","Done":false}

content を Query パラメータから外してリクエストします。

$ curl -vs -XGET "http://localhost:8080/user?done=false"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /user HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Date: 〜
< Content-Length: 0
<
* Connection #0 to host localhost left intact

Query パラメータに指定がないため、エラー(400 Bad Request)を返却しました。

Key: 'Task.Content' Error:Field validation for 'Content' failed on the 'required' tag

また API を起動したターミナルを見ると上記のようなメッセージが表示されています。 binding:"required" と指定したチェックでのエラー内容となります。

上記実装では Must bind である c.BindQuery を使っているため、必須エラーとなりエラーレスポンスの返却までおこなわれました。

これを Should bind を使用して実装すると以下の実装になります。 Should bind を使用するとレスポンスを自分で実装できるので、任意のレスポンスを返せるようになります。

	// 上記 GET /task の中を変更する
	r.GET("/task", func(c *gin.Context) {
		var task Task
		if err := c.ShouldBindQuery(&task); err != nil {
			// レスポンスのJsonにエラーの内容を設定している
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		} else {
			c.JSON(http.StatusOK, task)
		}
	})

先程エラーとなったリクエストを実行します。

$ curl -vs -XGET "http://localhost:8080/task?done=false"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /user HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< content-type: application/json; charset=utf-8
< Date: 〜
< Content-Length: 〜
<
* Connection #0 to host localhost left intact
{"error":"Key: 'Task.Content' Error:Field validation for 'Content' failed on the 'required' tag"}

Must bind の時と同じようにエラー(400 Bad Request)を返却できました。 また、レスポンスにエラー内容を出力できるようになりました。

上記では、 BindQuery , ShouldBindQuery を使ってみましたが、他にも以下ようのなものが用意されています。

  • Bind、BindJSON、BindXML、BindQuery、BindYAML、BindHeader …
  • ShouldBind、ShouldBindJSON、ShouldBindXML、ShouldBindQuery、ShouldBindYAML、ShouldBindHeader …

名前から推測できるように Json や XML などの値を struct に設定できるものなどがあります。 Bindcontent-type から自動で BindJSON, BindXML などを呼び分けてくれます。

また、使用可能なバリデーションについては、go-playground/validator/v10 にあるものが使用できます。 こちら がドキュメントとなります。 一般的な必須チェック、範囲チェック、文字列チェックから複数フィールドにまたがるチェックなど色々用意されています。

次にレスポンスについてですが、上記のコードでは Json を返すように実装し実際のレスポンスは以下となっていました。

c.JSON(http.StatusOK, user)
{"Content":"hoge","Done":false}

これは、Task struct の定義によって出力された Json となります。 Json のキー名も、先程の bind の時と同じように Go 言語にある encoding/json の json タグを指定することで 変更できます。

type Task struct {
	Content string `json:"content" form:"content" binding:"required"`
	Done    int    `json:"done" form:"done"`
}
$ curl -s -XGET "http://localhost:8080/task?content=hoge&done=false"
{"content":"hoge","done":false}

ここまでの機能を使い API を作っていきます。

リソースの定義

ますは、扱うリソースを Go 言語の struct を使い Task 情報として定義します。

type Task struct {
	Content string `json:"content" form:"content" binding:"required"`
 	Done    bool   `json:"done" form:"done"`
}

登録/取得API作成

タスク情報を登録する API を作成します。

上記に記載したタスク情報の Json を POST で受け取り登録する API を作成します。 登録と言っても RDB 等は使用せず、メモリに載せるだけにします。

また、登録情報を確認する必要もあるのでタスク情報を取得する API も作成します。

まず、タスク情報を受け取る API を作成します。 Json を受け取る struct は先程作成した Task struct を使います。

// 上記 GET /task の下に追記します
r.POST("/task", func(c *gin.Context) {
	var task Task
	if err := c.ShouldBind(&task); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		c.JSON(http.StatusOK, task)
	}
})

API を起動して動作を確認します。

POST を使用し Json でタスク情報を送ります。

$ curl -s -XPOST -H "content-type: application/json" -d '{"content":"hoge","done":false}' "http://localhost:8080/task"
{"content":"hoge","hoge":false}

保存する処理を追加します。 Go 言語の map を使い保存先を定義します。

var tasks = make(map[string]Task, 0)

また、タスク情報を識別できるように id を追加します。 タスク情報を取得する時や更新する時に使用します。

type User struct {
	ID      string `json:"id" form:"id" binding:"-"`
	Content string `json:"content" form:"content" binding:"required"`
 	Done    bool   `json:"done" form:"done"`
}

タスク情報を登録する処理を追加します。

先程用意した tasks に受け取ったタスク情報を追加します。 id はとりあえず xid を使いユニークな文字列を指定します。ユニークな文字列のため登録毎に値が変わります。

r.POST("/task", func(c *gin.Context) {
	var task Task
	if err := c.ShouldBind(&task); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		guid := xid.New()
		task.ID = guid.String()
		tasks[task.ID] = task // 登録
		c.JSON(http.StatusOK, task)
	}
})

API を起動して動作を確認します。

$ curl -s -XPOST -H "content-type: application/json" -d '{"content":"hoge","done":false}' "http://localhost:8080/task"
{"id":"c15df1f2hraih2ft6lt0","content":"hoge","done":false}

$ curl -s -XPOST -H "content-type: application/json" -d '{"content":"hoge1","done":true}' "http://localhost:8080/task"
{"id":"c15df472hraih2ft6ltg","content":"hoge1","done":true}

登録が確認できます。

続いて、タスク情報を取得する API を作成します。

上記で作成した GET /task を変更して作成します。 作成する API は、id を指定して対応したタスク情報を取得する API とします。

r.GET("/task/:id", func(c *gin.Context) {
	id := c.Param("id") // pathからidを取得
	task, ok := tasks[id] // 登録済みのタスク情報を取得
	if ok {
		c.JSON(http.StatusOK, task)
	} else {
		c.Status(http.StatusNotFound) // idに対応したタスク情報がない場合の処理
	}
})

API を起動して動作を確認します。

タスク情報を登録してから登録したタスク情報を取得します。

# タスク情報を登録

$ curl -s -XPOST -H "content-type: application/json" -d '{"content":"hoge","done":false}' "http://localhost:8080/task"
{"id":"c15df1f2hraih2ft6lt0","content":"hoge","done":false}

$ curl -s -XPOST -H "content-type: application/json" -d '{"content":"hoge1","done":true}' "http://localhost:8080/task"
{"id":"c15df472hraih2ft6ltg","content":"hoge1","done":true}
# タスク情報を取得

$ curl -s -XGET "http://localhost:8080/task/c15df1f2hraih2ft6lt0"
{"id":"c15df1f2hraih2ft6lt0","content":"hoge","done":false}

$ curl -s -XGET "http://localhost:8080/task/c15df472hraih2ft6ltg"
{"id":"c15df472hraih2ft6ltg","content":"hoge1","done":true}

未登録の id で取得すると存在しない旨のレスポンス(404 Not Found)が返ってきます。

$ curl -vs -XGET "http://localhost:8080/task/notfound"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /user/2 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: 〜
< Content-Length: 0
<
* Connection #0 to host localhost left intacts

登録及び取得の動作が確認できました。

更新/削除API作成

残りのタスク情報の更新 API, 削除 API を作成します。

更新 API は、 id とタスク情報を受け取って id に対応するタスク情報を更新します。

r.PUT("/task/:id", func(c *gin.Context) {
	var task Task
	if err := c.ShouldBind(&task); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	} else {
		id := c.Param("id")
		_, ok := tasks[id]
		if ok {
			task.ID = id
			tasks[id] = task
			c.JSON(http.StatusOK, task)
		} else {
			c.Status(http.StatusNotFound)
		}
	}
})

削除 API は、 id を受け取り対応するタスク情報を削除します。

r.DELETE("/task/:id", func(c *gin.Context) {
	id := c.Param("id")
	task, ok := tasks[id]
	if ok {
		delete(tasks, id)
		c.JSON(http.StatusOK, task)
	} else {
		c.Status(http.StatusNotFound)
	}
})

API を起動して動作を確認します。

# タスク情報を登録
$ curl -s -XPOST -H "content-type: application/json" -d '{"content":"hoge","done":false}' "http://localhost:8080/task"
{"id":"c15df1f2hraih2ft6lt0","content":"hoge","done":false}

# タスク情報を取得
$ curl -s -XGET "http://localhost:8080/task/c15df1f2hraih2ft6lt0"
{"id":"c15df1f2hraih2ft6lt0","content":"hoge","done":false}

# タスク情報を更新
$ curl -s -XPUT -H "content-type: application/json" -d '{"content":"hoge","done":true}' "http://localhost:8080/task/c15df1f2hraih2ft6lt0"
{"id":"c15df1f2hraih2ft6lt0","content":"hoge","done":true}

# タスク情報を取得
$ curl -s -XGET "http://localhost:8080/task/c15df1f2hraih2ft6lt0"
{"id":"c15df1f2hraih2ft6lt0","content":"hoge","done":true}

# タスク情報を削除 (削除した情報が表示される)
$ curl -s -XDELETE "http://localhost:8080/task/c15df1f2hraih2ft6lt0"
{"id":"c15df1f2hraih2ft6lt0","content":"hoge","done":true}

# タスク情報を取得
$ curl -vs -XGET "http://localhost:8080/task/c15df1f2hraih2ft6lt0"
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /task/0 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: 〜
< Content-Length: 0
<
* Connection #0 to host localhost left intact

一通りの機能が作成できました。

まとめ

RESTful な API を作成し、Gin Web Framework による Query 文字列やリクエスト Body の値を受け取る方法、 バリデーションの使い方を確認してみました。

実装方法には、Go 言語の StructTag が使用されていました。値のマッピングやバリデーションの定義が簡潔に書けるようになっていますね。

ソースはこちらに置いてあるので、興味のある方は触ってみてください。

機会をみてテストコードも書く予定です。

最後まで読んで頂きありがとうございます。