【Go言語】net/httpだけでREST APIを立ててみる

最近Goに入門しました。
勉強のため外部パッケージなしの手作りREST API立ててみました。
フルスクラッチでRESTってあまりやっている人がいなかったので記事にしてみます。（go-json-restはいっぱいあったんですが）

入門者なので、イケてない部分があれば（優しく）教えてください。

## やりたいこと

標準パッケージ（"net/http"）だけでREST APIを立てる。
外部パッケージは使わない。

## 最も単純な実装

ハンドラの中で`r.Method`条件分けすればOK。

```go
func main() {
	http.HandleFunc("/", restHandler)
	http.ListenAndServe(":8080", nil)
}

func restHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		fmt.Fprintln(w, "GET called!!")
	} else if r.Method == "POST" {
		fmt.Fprintln(w, "POST called!!")
	} else if r.Method == "PUT" {
		fmt.Fprintln(w, "PUT called!!")
	} else if r.Method == "DELETE" {
		fmt.Fprintln(w, "DELETE called!!")
	}
}
```

```sh
$ curl -X GET localhost:8080
GET called!!
$ curl -X POST localhost:8080
POST called!!
$ curl -X PUT localhost:8080 
PUT called!!
$ curl -X DELETE localhost:8080
DELETE called!!
```

## Todoリスト用APIの実装
上で終わり、あとは各自好きなようにハンドラ書いてね、っていうのは記事としていかがかと思うので、簡単なTodoAPIを立てて、[Nuxt.jsで以前作ったフロントエンド](https://qiita.com/daitai-daidai/items/a5e744120492bcc2c591)から操作できるようにしてみました。

### API仕様

|エンドポイント|リクエストメソッド|動作|
| ---- | ---- | ---- |
|`/items`|GET|全てのタスクの取得|
|`/items`|POST|新規アイテムの追加|
|`/items`|DELETE|実行済み全タスクの削除|
|`/items/:id`|DELETE|1つのタスクを削除|
|`/items/:id/done`|PUT|1つのタスクを実行済みにする|
|`/items/:id/done`|DELETE|1つのタスクを実行済みに変更|

### テーブル
TodoのItemsテーブルに以下の項目を用意。

- id （主キー）
- name （Todoアイテムの名前）
- done （実行済みか否か）

```go
// 構造体
type Item struct {
	Id   int    `json:"id"`
	Name string `json:"name"`
	Done bool   `json:"done"`
}

// テーブル
func initDB(db *sql.DB) error {
	const sql = `
		CREATE TABLE IF NOT EXISTS items (
			id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
			name TEXT NOT NULL,
			done BOOLEAN NOT NULL DEFAULT 0
		);`
	_, err := db.Exec(sql)
	return err
}
```

### リクエストハンドラ
2つのルート`/items`、`/items/`（`/items/:id`と`/items/:id/done`）のそれぞれにハンドラ関数を設定しました。各ハンドラの中でルートパラメタとリクエストメソッドで場合わけし、データベース操作を行います。

```go
func main() {
	// （略）データベース初期化

	// リクエストハンドラの追加
	http.HandleFunc("/items", itemsHandler)    // `/items`の処理（）
	http.HandleFunc("/items/", itemsIdHandler) // `/items/:id`と`/items/:id/done`の処理

	err = http.ListenAndServe(":4000", nil)
	if err != nil {
		log.Fatal(err)
	}
}

/*** リクエストハンドラ ***/

func itemsHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "GET":
		getAllItems(w, r) // 全てのitemの取得
	case "POST":
		addNewItem(w, r) // 新しいitemの追加
	case "DELETE":
		deleteDoneItems(w) // 実行済みitemの削除
	default:
		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
	}
}

func itemsIdHandler(w http.ResponseWriter, r *http.Request) {
	// ルートパラメータの取得（例: `/items/1/done` -> ["items", "1", "done"]）
	params := getRouteParams(r)
	if len(params) < 2 || len(params) > 3 {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	// itemのidをintで取得
	id, err := strconv.Atoi(params[1])
	if err != nil || id < 1 {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	if len(params) == 2 {
		updateItem(id, w, r)
	} else if params[2] == "done" {
		updateDone(id, w, r)
	} else {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
	}
}

func updateItem(id int, w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "DELETE":
		deleteOneItem(id, w)
	default:
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
	}
}

func updateDone(id int, w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "PUT":
		doneItem(id, w, r)
	case "DELETE":
		unDoneItem(id, w, r)
	default:
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
	}
}
```

データベースの操作はこんな感じです。

```go
// 全アイテムの取得
func getAllItems(w http.ResponseWriter, r *http.Request) {
	var items []Item
	rows, err := db.Query(`SELECT * FROM items;`)
	if err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
	}
	for rows.Next() {
		var item Item
		rows.Scan(&item.Id, &item.Name, &item.Done)
		items = append(items, item)
	}
	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	if err := enc.Encode(items); err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
	}
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprint(w, buf.String())
}

// 新しいアイテムを追加
func addNewItem(w http.ResponseWriter, r *http.Request) {
	var reqBody struct {
		Name string `json:"name"`
	}
	dec := json.NewDecoder(r.Body)
	err := dec.Decode(&reqBody)
	if err != nil {
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
	}
	_, err = db.Exec(`INSERT INTO items (name, done) values (?, ?)`, reqBody.Name, false)
	if err != nil {
		log.Print(err)
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
	}
	w.WriteHeader(http.StatusCreated)
}

// 1つのアイテムを削除
func deleteOneItem(id int, w http.ResponseWriter) {
	_, err := db.Exec(`DELETE FROM items WHERE id=?`, id)
	if err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
	}
	w.WriteHeader(http.StatusOK)
}

// 全ての実行済みアイテムを削除
func deleteDoneItems(w http.ResponseWriter) {
	_, err := db.Exec(`DELETE FROM items WHERE done=true`)
	if err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
	}
	w.WriteHeader(http.StatusOK)
}

// アイテムを実行済みにする
func doneItem(id int, w http.ResponseWriter, r *http.Request) {
	if id == -1 {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}
	_, err := db.Exec(`UPDATE items SET done=true where id=?`, id)
	if err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
	}
	w.WriteHeader(http.StatusCreated)
}

// アイテムを未実行にする
func unDoneItem(id int, w http.ResponseWriter, r *http.Request) {
	if id == -1 {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}
	_, err := db.Exec(`UPDATE items SET done=false where id=?`, id)
	if err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
	}
	w.WriteHeader(http.StatusOK)
}
```

ちなみにルートパラメータの取得もいい関数が見当たらなかったので`strings.Split` などを使って自分で書きました。（イケてるかあまり自信ない。）

```go
func getRouteParams(r *http.Request) []string {
	splited := strings.Split(r.RequestURI, "/")
	var params []string
	for i := 0; i < len(splited); i++ {
		if len(splited[i]) != 0 {
			params = append(params, splited[i])
		}
	}
	return params
}
```

### 動作確認

curlで動作確認。

```sh
# タスクを5つ追加
$ curl -d '{"name":"item1"}' localhost:4000/items
$ curl -d '{"name":"item2"}' localhost:4000/items
$ curl -d '{"name":"item3"}' localhost:4000/items
$ curl -d '{"name":"item4"}' localhost:4000/items  
$ curl -d '{"name":"item5"}' localhost:4000/items

# 全タスクの取得
$ curl localhost:4000/items
[{"id":1,"name":"item1","done":false},{"id":2,"name":"item2","done":false},{"id":3,"name":"item3","done":false},{"id":4,"name":"item4","done":false},{"id":5,"name":"item5","done":false}]

# item2, 3, 4を実行済みにする
$ curl -X PUT localhost:4000/items/2/done
$ curl -X PUT localhost:4000/items/3/done
$ curl -X PUT localhost:4000/items/4/done
$ curl localhost:4000/items              
[{"id":1,"name":"item1","done":false},{"id":2,"name":"item2","done":true},{"id":3,"name":"item3","done":true},{"id":4,"name":"item4","done":true},{"id":5,"name":"item5","done":false}]

# item3を未実行にする
$ curl -X DELETE localhost:4000/items/3/done
$ curl localhost:4000/items                 
[{"id":1,"name":"item1","done":false},{"id":2,"name":"item2","done":true},{"id":3,"name":"item3","done":false},{"id":4,"name":"item4","done":true},{"id":5,"name":"item5","done":false}]

# 実行済みを全て削除する
$ curl -X DELETE localhost:4000/items
$ curl localhost:4000/items/          
[{"id":1,"name":"item1","done":false},{"id":3,"name":"item3","done":false},{"id":5,"name":"item5","done":false}]

# アイテムを1つ削除する
$ curl -X DELETE localhost:4000/items/3
$ curl localhost:4000/items         
[{"id":1,"name":"item1","done":false},{"id":5,"name":"item5","done":false}]
```

基本的なCRUDはうまくいっているようです。

## CORSの対応
curl叩けば行くのですが、ブラウザからaxios経由でやるとうまくいきません。

![スクリーンショット 2021-04-14 16.52.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/121ce72a-6389-aab7-681e-1dc0e1e81ad4.png)

CORS（オリジン間リソース共有）を許可するために、レスポンスヘッダを追加します。

- オリジン間アクセスを許可する`Access-Control-Allow-Origin`ヘッダ。
- axios（の中で呼ばれているXMLHttpRequest）がPOST/PUT/DELETEのリクエストする前にはOPTIONSリクエストが飛んでくる(Pre-Flightリクエスト)それに対する`Access-Control-Allow-Headers`と`Access-Control-Allow-Methods`ヘッダ。

```go
func itemsHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") // localhost:3000からのオリジン間アクセスを許可する

	switch r.Method {
	case "GET":
		getAllItems(w, r) // 全てのitemの取得
	case "POST":
		addNewItem(w, r) // 新しいitemの追加
	case "DELETE":
		deleteDoneItems(w) // 実行済みitemの削除
	case "OPTIONS":
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")               // Content-Typeヘッダの使用を許可する
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS") // pre-flightリクエストに対応する
	default:
		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
	}
}

func itemsIdHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") // localhost:3000からのオリジン間アクセスを許可する

	// ルートパラメータの取得（例: `/items/1/done` -> ["items", "1", "done"]）
	params := getRouteParams(r)
	if len(params) < 2 || len(params) > 3 {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	// itemのidをintで取得
	id, err := strconv.Atoi(params[1])
	if err != nil || id < 1 {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	if len(params) == 2 {
		updateItem(id, w, r)
	} else if params[2] == "done" {
		updateDone(id, w, r)
	} else {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
	}
}

func updateItem(id int, w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "DELETE":
		deleteOneItem(id, w)
	case "OPTIONS":
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")    // Content-Typeヘッダの使用を許可する
		w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS") // pre-flightリクエストに対応する
	default:
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
	}
}

func updateDone(id int, w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "PUT":
		doneItem(id, w, r)
	case "DELETE":
		unDoneItem(id, w, r)
	case "OPTIONS":
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")         // Content-Typeヘッダの使用を許可する
		w.Header().Set("Access-Control-Allow-Methods", "PUT, DELETE, OPTIONS") // pre-flightリクエストに対応する
	default:
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
	}
}
```

できました。

![todoAPI.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677011/e6d3741b-45fe-2c24-8908-faf13cda12f6.gif)

## まとめ

Goのnet/httpパッケージのみで簡単なREST APIを立ててみました。フルスクラッチでも割と簡単にかけてしまうので良いですね。
今、42Tokyoの課題でC++のフルスクラッチWebサーバも開発しているのですが手軽さでは比較になりませんね。標準パッケージでこんなに簡単に書けて素晴らしい。（net/httpの再実装をしたらすごく勉強になりそう）

完全なるコードはこちら。改善点などあればコメントいただけますと幸いです。
API：https://github.com/dai65527/go_simpleREST
フロントエンド：https://github.com/dai65527/tstodo-client