chi で RESTful API を設計する:CRUD エンドポイントの実装例
chi を使えば、RESTful な CRUD エンドポイントをすっきり整理できます。標準ライブラリだけでは冗長になりがちなルーティングも、chi の Route や URLParam を活用すればシンプルにまとまります。
プロジェクトの全体像
ユーザーリソースに対する CRUD(Create / Read / Update / Delete)を実装してみましょう。まずはルーティングの全体像を示します。
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"sync"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var (
users = make(map[int]User)
nextID = 1
mu sync.Mutex
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Get("/{id}", getUser)
r.Put("/{id}", updateUser)
r.Delete("/{id}", deleteUser)
})
fmt.Println("サーバー起動: http://localhost:8080")
http.ListenAndServe(":8080", r)
}r.Route("/users", ...) でプレフィックスをまとめ、その中に各メソッドのハンドラを登録しています。{id} は URL パラメータで、chi.URLParam で取得できます。
| メソッド | パス | 操作 |
|---|---|---|
| GET | /users | 一覧取得 |
| POST | /users | 新規作成 |
| GET | /users/{id} | 個別取得 |
| PUT | /users/{id} | 更新 |
| DELETE | /users/{id} | 削除 |
一覧取得(GET /users)
全ユーザーを JSON 配列で返すハンドラです。
func listUsers(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
list := make([]User, 0, len(users))
for _, u := range users {
list = append(list, u)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(list)
}json.NewEncoder(w).Encode を使えば、構造体やスライスを直接レスポンスに書き出せます。json.Marshal でバイト列に変換してから w.Write する方法もありますが、Encoder のほうがメモリ効率がよいです。
Content-Type ヘッダーを application/json に設定するのを忘れないようにしましょう。設定しないとクライアント側で JSON として認識されない場合があります。
新規作成(POST /users)
リクエストボディから JSON を受け取って新しいユーザーを作成します。
func createUser(w http.ResponseWriter, r *http.Request) {
var input struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "リクエストの JSON が不正です", http.StatusBadRequest)
return
}
if input.Name == "" || input.Email == "" {
http.Error(w, "name と email は必須です", http.StatusBadRequest)
return
}
mu.Lock()
user := User{ID: nextID, Name: input.Name, Email: input.Email}
users[nextID] = user
nextID++
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}json.NewDecoder(r.Body).Decode でリクエストボディをパースしています。バリデーションは最低限として、空文字のチェックだけ入れました。本番ではより厳密なバリデーションが必要になるでしょう。
新規作成に成功したら http.StatusCreated(201)を返すのが RESTful な慣習です。200 でも動きますが、クライアントにとっては 201 のほうが意味が明確になります。
個別取得(GET /users/{id})
URL パラメータから ID を取得し、該当するユーザーを返します。
func getUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "ID が不正です", http.StatusBadRequest)
return
}
mu.Lock()
user, ok := users[id]
mu.Unlock()
if !ok {
http.Error(w, "ユーザーが見つかりません", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}chi.URLParam(r, "id") でパスパラメータを文字列として取得し、strconv.Atoi で整数に変換しています。存在しない ID の場合は 404 を返すのがセオリーです。
更新(PUT /users/{id})
既存ユーザーの情報を上書きします。
func updateUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "ID が不正です", http.StatusBadRequest)
return
}
var input struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "リクエストの JSON が不正です", http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
if _, ok := users[id]; !ok {
http.Error(w, "ユーザーが見つかりません", http.StatusNotFound)
return
}
user := User{ID: id, Name: input.Name, Email: input.Email}
users[id] = user
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}PUT は「リソース全体の置き換え」を意味します。部分更新なら PATCH を使うのが REST の慣習ですが、シンプルな API であれば PUT だけで問題ありません。
削除(DELETE /users/{id})
指定した ID のユーザーを削除します。
func deleteUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
http.Error(w, "ID が不正です", http.StatusBadRequest)
return
}
mu.Lock()
defer mu.Unlock()
if _, ok := users[id]; !ok {
http.Error(w, "ユーザーが見つかりません", http.StatusNotFound)
return
}
delete(users, id)
w.WriteHeader(http.StatusNoContent)
}削除成功時は http.StatusNoContent(204)を返します。レスポンスボディは空です。
削除結果をボディで返す方式。クライアントが削除結果を確認しやすい。
ボディなしで「処理完了」を伝える方式。REST の慣習に沿っており、帯域の節約にもなる。
curl でテストする
サーバーを起動したら、curl で各エンドポイントを試してみましょう。
# ユーザー作成
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'
# 一覧取得
curl http://localhost:8080/users
# 個別取得
curl http://localhost:8080/users/1
# 更新
curl -X PUT http://localhost:8080/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"Alice Updated","email":"alice@new.com"}'
# 削除
curl -X DELETE http://localhost:8080/users/1この例ではデータをメモリ上の map に保存しているため、サーバーを再起動するとデータは消えます。永続化が必要な場合は GORM などの ORM やデータベースドライバを組み合わせてください。