chi で RESTful API を設計する:CRUD エンドポイントの実装例

chi を使えば、RESTful な CRUD エンドポイントをすっきり整理できます。標準ライブラリだけでは冗長になりがちなルーティングも、chi の RouteURLParam を活用すればシンプルにまとまります。

プロジェクトの全体像

ユーザーリソースに対する 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)を返します。レスポンスボディは空です。

200 OK + メッセージ

削除結果をボディで返す方式。クライアントが削除結果を確認しやすい。

204 No Content

ボディなしで「処理完了」を伝える方式。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 やデータベースドライバを組み合わせてください。