Goで作成したToDoアプリにRESTful APIを追加してみる

# 
[Go x Gin x GORMでToDoアプリを作成してみる](https://qiita.com/hanadaUG/items/0fd5ddc0407c4ab55d0f)で作成したToDoアプリに追加する形で機能拡張や使用したことのない技術を試していきます。
今回はCRUDのRESTful APIを追加します。

# 開発環境
- MacBook Pro
- go version : 1.12.9

```terminal:version
 $ go version
go version go1.12.9 darwin/amd64
```

# 事前情報
Webフレームワーク : [Gin](https://github.com/gin-gonic/gin)
ORM : [GORM](https://gorm.io/docs)
DB : sqlite3
依存関係管理ツールdep : dep


```terminal:プロジェクト構成
├── Gopkg.lock
├── Gopkg.toml
├── README.md
├── api
│   └── v1
│       └── api_task_controller.go
├── controllers
│   └── task_controller.go
├── db
│   └── db.go
├── main.go
├── models
│   └── task.go
├── router
│   └── router.go
├── task.db
├── templates
│   ├── edit.html
│   └── index.html
└── vendor
```
ソースコードは[GitHub](https://github.com/hanadaUG/go-gin-gorm-todo-app/pull/2)にアップロードしています。

# api/v1/api_task_controller.go
CRUDの挙動を記述しています。

```go:api/v1/api_task_controller.go
package v1

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/hanadaUG/go-gin-gorm-todo-app/models"
	"github.com/jinzhu/gorm"
	"net/http"
)

type ApiTaskHandler struct {
	Db *gorm.DB
}

// 全件取得
// $ curl -X GET -H "Content-Type: application/json" http://localhost:8080/api/v1/ | jsonpp
func (handler *ApiTaskHandler) GetAll(c *gin.Context) {
	var tasks []models.Task      // レコード一覧を格納するため、Task構造体のスライスを変数宣言
	handler.Db.Find(&tasks)      // DBから全てのレコードを取得する
	c.JSON(http.StatusOK, tasks) // JSONで全てのレコードを渡す
}

// 新規作成
// $ curl -X POST -H "Content-Type: application/json" -d '{"text":"test"}' http://localhost:8080/api/v1/ | jsonpp
func (handler *ApiTaskHandler) Create(c *gin.Context) {
	task := models.Task{}
	err := c.BindJSON(&task)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	handler.Db.Create(&task)     // レコードを挿入する
	c.JSON(http.StatusOK, &task) // JSONで結果を返す
}

// 取得
// $ curl -X GET -H "Content-Type: application/json" http://localhost:8080/api/v1/1 | jsonpp
func (handler *ApiTaskHandler) Get(c *gin.Context) {
	task := models.Task{}       // Task構造体の変数宣言
	id := c.Param("id")         // リクエストからidを取得
	handler.Db.First(&task, id) // idに一致するレコードを取得する
	c.JSON(http.StatusOK, task) // JSONで取得したレコードを返す
}

// 更新
// $ curl -X PUT -H "Content-Type: application/json" -d '{"text":"update"}' http://localhost:8080/api/v1/1 | jsonpp
func (handler *ApiTaskHandler) Update(c *gin.Context) {
	task := models.Task{}       // Task構造体の変数宣言
	id := c.Param("id")         // idを取得
	handler.Db.First(&task, id) // idに一致するレコードを取得する
	request := models.Task{}    // Task構造体の変数宣言
	err := c.BindJSON(&request)
	if err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	task.Text = request.Text     // textを上書きする
	handler.Db.Save(&task)       // 指定のレコードを更新する
	c.JSON(http.StatusOK, &task) // JSONで結果を返す
}

// 削除
// $ curl -X DELETE -H "Content-Type: application/json" http://localhost:8080/api/v1/1 | jsonpp
func (handler *ApiTaskHandler) Delete(c *gin.Context) {
	task := models.Task{}       // Task構造体の変数宣言
	id := c.Param("id")         // リクエストからidを取得
	handler.Db.First(&task, id) // idに一致するレコードを取得する
	handler.Db.Delete(&task)    // 指定のレコードを削除する
	msg := fmt.Sprintf("Task [%s] has been deleted.", id)
	c.JSON(http.StatusOK, gin.H{
		"message": msg,
	})
}
```

# router/router.go
ルーティングの設定を行います。


```diff:router/router.go
@@ -2,6 +2,7 @@ package router
 
 import (
        "github.com/gin-gonic/gin"
+       v1 "github.com/hanadaUG/go-gin-gorm-todo-app/api/v1"
        "github.com/hanadaUG/go-gin-gorm-todo-app/controllers"
        "github.com/hanadaUG/go-gin-gorm-todo-app/db"
 )
@@ -24,10 +25,27 @@ func Router() {
        // 第一引数にパス、第二引数にHandlerを登録する
        router.GET("/", handler.GetAll)            // 一覧表示
        router.POST("/", handler.Create)           // 新規作成
-       router.GET("/:id", handler.Edit)           // 編集画面
+       router.GET("/edit/:id", handler.Edit)      // 編集画面
        router.POST("/update/:id", handler.Update) // 更新
        router.POST("/delete/:id", handler.Delete) // 削除
 
+       // 共通の /api/v1 をパスに持つルートをグループ化する
+       apiV1 := router.Group("/api/v1")
+       {
+               api := v1.ApiTaskHandler{
+                       Db: db.Get(),
+               }
+               // 編集画面の /:id というパスは :id をワイルドカードとして http://localhost:8080/1 だけではなく
+               // http://localhost:8080/api/vi/ も同一パスとして判定してしまう
+               // そのため /edit/:id に変更した
+               // panic: 'api' in new path '/api/v1/' conflicts with existing wildcard ':id' in existing prefix '/:id'
+               apiV1.GET("/", api.GetAll)       // 全件取得
+               apiV1.POST("/", api.Create)      // 新規作成
+               apiV1.GET("/:id", api.Get)       // 取得
+               apiV1.PUT("/:id", api.Update)    // 更新
+               apiV1.DELETE("/:id", api.Delete) // 削除
+       }
+
        // Routerをhttp.Serverに接続し、HTTPリクエストのリスニングとサービスを開始する
        router.Run()
 }
```

# templates/index.html
```diff:templates/index.html
@@ -16,7 +16,7 @@
     {{ range .tasks }}
         <li>
             <form method="POST" action="/delete/{{.ID}}">
-                {{.ID}} : {{ .Text }} [<a href="/{{.ID}}">編集</a>]
+                {{.ID}} : {{ .Text }} [<a href="/edit/{{.ID}}">編集</a>]
                 <input type="submit" value="削除">
             </form>
         </li>
```


# 動作確認

```:起動
# Go製ツールでjsonを整形するjsonppをインストールする
# https://jmhodges.github.io/jsonpp/
$ brew install jsonpp

# アプリを起動する
$ go run main.go 

[GIN-debug] Listening and serving HTTP on :8080

# curlコマンドでAPIをテストする
$ curl -X GET -H "Content-Type: application/json" http://localhost:8080/api/v1/ | jsonpp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   135  100   135    0     0  18223      0 --:--:-- --:--:-- --:--:-- 22500
[
  {
    "ID": 1,
    "CreatedAt": "2019-11-04T12:51:05.009403+09:00",
    "UpdatedAt": "2019-11-04T12:51:05.009403+09:00",
    "DeletedAt": null,
    "Text": "test"
  }
]
```


ソースコードは[GitHub](https://github.com/hanadaUG/go-gin-gorm-todo-app/pull/2)にアップロードしています。