LoginSignup
17
8

More than 3 years have passed since last update.

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

Posted at

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

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

やりたいこと

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

最も単純な実装

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

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!!")
    }
}
$ 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で以前作ったフロントエンドから操作できるようにしてみました。

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 (実行済みか否か)
// 構造体
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)のそれぞれにハンドラ関数を設定しました。各ハンドラの中でルートパラメタとリクエストメソッドで場合わけし、データベース操作を行います。

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)
    }
}

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

// 全アイテムの取得
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 などを使って自分で書きました。(イケてるかあまり自信ない。)

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で動作確認。

# タスクを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

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

  • オリジン間アクセスを許可するAccess-Control-Allow-Originヘッダ。
  • axios(の中で呼ばれているXMLHttpRequest)がPOST/PUT/DELETEのリクエストする前にはOPTIONSリクエストが飛んでくる(Pre-Flightリクエスト)それに対するAccess-Control-Allow-HeadersAccess-Control-Allow-Methodsヘッダ。
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

まとめ

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

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

17
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
8