REST API開発
REST API開発
現実世界のアナロジー
レストランを経営していると想像してください。お客様(クライアント)がフロントデスクに来店し、ウェイター(ルーター)がニーズに基づいて異なる窓口に案内します:
- メニューを確認 → フロントデスクの問い合わせ窓口(GET)
- 注文する → 注文窓口(POST)
- 注文を変更 → 注文変更窓口(PUT)
- 注文をキャンセル → キャンセル窓口(DELETE)
キッチン(サーバー)がリクエストを処理し、準備された料理(レスポンス)をウェイターを通じてお客様に届けます。同時に、警備員(ミドルウェア)がお客様が入る前に身元を確認し、来店時間を記録し、緊急事態に対応できます。
REST APIはこのような標準化されたサービスワークフローです。クライアントは統一された「動詞」(HTTPメソッド)と「アドレス」(URL)を使用してサーバーと通信し、サーバーは構造化されたデータ(JSON)を返します。
プロジェクト要件
以下の機能をサポートするTodo REST APIを開発します:
| 操作 | メソッド | パス | 説明 |
|---|---|---|---|
| すべてのTodoを取得 | GET |
/api/todos |
Todoリストを返す |
| 単一のTodoを取得 | GET |
/api/todos/{id} |
IDで詳細を返す |
| Todoを作成 | POST |
/api/todos |
新しいTodoを追加 |
| Todoを更新 | PUT |
/api/todos/{id} |
指定されたTodoを変更 |
| Todoを削除 | DELETE |
/api/todos/{id} |
指定されたTodoを削除 |
追加要件:
- インメモリストレージ(map + mutex)を使用
- ミドルウェアチェーンを実装:ログ記録、認証、パニック回復
- リクエストとレスポンスの両方にJSON形式を使用
システム設計
クライアントリクエスト
│
▼
┌──────────────────────────────┐
│ ミドルウェアチェーン │
│ Recovery → Auth → Logging │
│ ↓ ↓ ↓ │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ ルーター │
│ GET /api/todos │
│ GET /api/todos/{id} │
│ POST /api/todos │
│ PUT /api/todos/{id} │
│ DELETE /api/todos/{id} │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ インメモリストレージレイヤー │
│ map[string]Todo + sync.RWMutex │
└──────────────────────────────┘
例 1: 完全なコード
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
// Todoアイテムの構造体
type Todo struct {
ID string `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// TodoStoreはインメモリストレージで、読み書きロックによる並行安全性を確保
type TodoStore struct {
mu sync.RWMutex
todos map[string]Todo
}
// NewTodoStoreは新しいストレージインスタンスを作成する
func NewTodoStore() *TodoStore {
return &TodoStore{
todos: make(map[string]Todo),
}
}
// GetByIDはIDで単一のTodoアイテムを返す
func (s *TodoStore) GetByID(id string) (Todo, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
t, ok := s.todos[id]
return t, ok
}
// GetAllはすべてのTodoアイテムを返す
func (s *TodoStore) GetAll() []Todo {
s.mu.RLock()
defer s.mu.RUnlock()
list := make([]Todo, 0, len(s.todos))
for _, t := range s.todos {
list = append(list, t)
}
return list
}
// Createは新しいTodoアイテムを作成する
func (s *TodoStore) Create(title string) Todo {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().Format(time.RFC3339)
t := Todo{
ID: uuid.New().String(),
Title: title,
Completed: false,
CreatedAt: now,
UpdatedAt: now,
}
s.todos[t.ID] = t
return t
}
// Updateは指定されたTodoアイテムを更新する
func (s *TodoStore) Update(id string, title string, completed bool) (Todo, bool) {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.todos[id]
if !ok {
return Todo{}, false
}
t.Title = title
t.Completed = completed
t.UpdatedAt = time.Now().Format(time.RFC3339)
s.todos[id] = t
return t, true
}
// Deleteは指定されたTodoアイテムを削除する
func (s *TodoStore) Delete(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.todos[id]; !ok {
return false
}
delete(s.todos, id)
return true
}
// --- 共通レスポンス構造体 ---
// APIResponseは統一APIレスポンス形式
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// jsonResponseはJSONレスポンスを送信する
func jsonResponse(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// --- ミドルウェア ---
// Middleware型定義
type Middleware func(http.Handler) http.Handler
// RecoveryMiddlewareはパニック回復ミドルウェア
// ハンドラ関数内のパニックをキャッチし、プロセスをクラッシュさせる代わりに500エラーを返す
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
jsonResponse(w, http.StatusInternalServerError, APIResponse{
Success: false,
Error: "内部サーバーエラー",
})
}
}()
next.ServeHTTP(w, r)
})
}
// LoggingMiddlewareはリクエストログミドルウェア
// 各リクエストのメソッド、パス、所要時間を記録する
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("[LOG] %s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
// AuthMiddlewareはシンプルな認証ミドルウェア
// リクエストヘッダーに有効なAPI Keyが含まれているかを確認する
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
apiKey := r.Header.Get("X-API-Key")
if apiKey != "secret-key-123" {
jsonResponse(w, http.StatusUnauthorized, APIResponse{
Success: false,
Error: "無効なAPIキー",
})
return
}
next.ServeHTTP(w, r)
})
}
// Chainは複数のミドルウェアをチェーンに結合する
// パラメータの順序が実行順序:Chain(A, B, C) → A(B(C(handler)))
func Chain(middlewares ...Middleware) Middleware {
return func(next http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
next = middlewares[i](next)
}
return next
}
}
// --- ルーターとハンドラ ---
// Routerはカスタムルーター
type Router struct {
routes map[string]map[string]http.HandlerFunc // method -> path -> handler
middleware Middleware
}
// NewRouterはルーターを作成する
func NewRouter() *Router {
return &Router{
routes: make(map[string]map[string]http.HandlerFunc),
}
}
// Useはグローバルミドルウェアを登録する
func (rt *Router) Use(m Middleware) {
rt.middleware = m
}
// Handleはルートを登録する:メソッド + パス + ハンドラ
func (rt *Router) Handle(method, path string, handler http.HandlerFunc) {
if _, ok := rt.routes[method]; !ok {
rt.routes[method] = make(map[string]http.HandlerFunc)
}
rt.routes[method][path] = handler
}
// ServeHTTPはhttp.Handlerインターフェースを実装し、ルートマッチングを完了する
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
methodRoutes, ok := rt.routes[r.Method]
if !ok {
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{
Success: false,
Error: "メソッドが許可されていません",
})
return
}
// 完全一致
if handler, ok := methodRoutes[r.URL.Path]; ok {
handler(w, r)
return
}
// プレフィックス一致:パスパラメータの抽出を試みる(例:/api/todos/{id})
for pattern, handler := range methodRoutes {
if strings.HasSuffix(pattern, "/{id}") {
prefix := strings.TrimSuffix(pattern, "/{id}")
if strings.HasPrefix(r.URL.Path, prefix+"/") {
// {id}をリクエストヘッダーに入れてハンドラに読み取らせる
id := strings.TrimPrefix(r.URL.Path, prefix+"/")
r.Header.Set("X-Resource-ID", id)
handler(w, r)
return
}
}
}
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "リソースが見つかりません",
})
}
// --- ハンドラ関数 ---
// handleGetTodosはすべてのTodoアイテムを返す
func handleGetTodos(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
todos := store.GetAll()
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: todos,
})
}
}
// handleGetTodoは単一のTodoアイテムを返す
func handleGetTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Resource-ID")
todo, ok := store.GetByID(id)
if !ok {
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "Todoアイテムが見つかりません",
})
return
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: todo,
})
}
}
// CreateTodoRequestはTodo作成のリクエストボディ
type CreateTodoRequest struct {
Title string `json:"title"`
}
// handleCreateTodoはTodoアイテムを作成する
func handleCreateTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req CreateTodoRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "無効なリクエストボディ",
})
return
}
if strings.TrimSpace(req.Title) == "" {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "タイトルは空にできません",
})
return
}
todo := store.Create(req.Title)
jsonResponse(w, http.StatusCreated, APIResponse{
Success: true,
Data: todo,
})
}
}
// UpdateTodoRequestはTodo更新のリクエストボディ
type UpdateTodoRequest struct {
Title string `json:"title"`
Completed bool `json:"completed"`
}
// handleUpdateTodoはTodoアイテムを更新する
func handleUpdateTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Resource-ID")
var req UpdateTodoRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "無効なリクエストボディ",
})
return
}
if strings.TrimSpace(req.Title) == "" {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "タイトルは空にできません",
})
return
}
todo, ok := store.Update(id, req.Title, req.Completed)
if !ok {
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "Todoアイテムが見つかりません",
})
return
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: todo,
})
}
}
// handleDeleteTodoはTodoアイテムを削除する
func handleDeleteTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Resource-ID")
if !store.Delete(id) {
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "Todoアイテムが見つかりません",
})
return
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: "正常に削除しました",
})
}
}
func main() {
// ストレージを初期化
store := NewTodoStore()
// ルーターを作成
router := NewRouter()
// ミドルウェアチェーンを登録:Recovery → Auth → Logging
router.Use(Chain(RecoveryMiddleware, AuthMiddleware, LoggingMiddleware))
// ルートを登録
router.Handle("GET", "/api/todos", handleGetTodos(store))
router.Handle("GET", "/api/todos/{id}", handleGetTodo(store))
router.Handle("POST", "/api/todos", handleCreateTodo(store))
router.Handle("PUT", "/api/todos/{id}", handleUpdateTodo(store))
router.Handle("DELETE", "/api/todos/{id}", handleDeleteTodo(store))
// ミドルウェアを適用してサーバーを起動
addr := ":8080"
log.Printf("REST APIサーバーが http://localhost%s で起動しました", addr)
log.Fatal(http.ListenAndServe(addr, router.middleware(router)))
}
実行とテスト
サービスを起動する前に、モジュールを初期化し依存関係をインストールします:
go mod init todo-api
go get github.com/google/uuid
go run main.go
curlを使用して各エンドポイントをテストします:
# Todoアイテムを作成
curl -X POST http://localhost:8080/api/todos \
-H "Content-Type: application/json" \
-H "X-API-Key: secret-key-123" \
-d '{"title": "Goを学ぶ"}'
# すべてのTodoを取得
curl http://localhost:8080/api/todos \
-H "X-API-Key: secret-key-123"
# 単一のTodoを取得({id}を実際の返されたIDに置き換えてください)
curl http://localhost:8080/api/todos/{id} \
-H "X-API-Key: secret-key-123"
# Todoを更新
curl -X PUT http://localhost:8080/api/todos/{id} \
-H "Content-Type: application/json" \
-H "X-API-Key: secret-key-123" \
-d '{"title": "Goを学ぶ(完了)", "completed": true}'
# Todoを削除
curl -X DELETE http://localhost:8080/api/todos/{id} \
-H "X-API-Key: secret-key-123"
# 認証なしリクエストをテスト(401が返されるはず)
curl http://localhost:8080/api/todos
期待されるレスポンスの例:
# 正常に作成
{"success":true,"data":{"id":"a1b2c3d4-...","title":"Goを学ぶ","completed":false,"created_at":"2026-06-27T10:00:00+08:00","updated_at":"2026-06-27T10:00:00+08:00"}}
# 認証なし
{"success":false,"error":"無効なAPIキー"}
コード解析
1. データモデルとストレージレイヤー
Todo構造体はTodoアイテムのデータ構造を定義し、JSONタグを使用してシリアライズされたフィールド名を制御します。TodoStoreはsync.RWMutexを使用してmapを保護します。読み取り操作は読み取りロック(RLock)を使用し、書き込み操作は書き込みロック(Lock)を使用して並行安全性を確保します。
2. ミドルウェアパターン
各ミドルウェアはfunc(http.Handler) http.Handler型で、デコレータパターンを通じて次のハンドラをラップします:
RecoveryMiddleware(
AuthMiddleware(
LoggingMiddleware(
final handler,
),
),
)
- RecoveryMiddleware:
defer + recoverを使用してパニックをキャッチ - AuthMiddleware:リクエストヘッダーのAPI Keyを確認
- LoggingMiddleware:リクエストメソッド、パス、所要時間を記録
Chain関数は複数のミドルウェアをパラメータの順序でチェーンに結合します。
3. カスタムルーター
Go 1.22以前では標準ライブラリのhttp.ServeMuxがパスパラメータをサポートしていなかったため、シンプルなルーターを実装しました:
- ネストされた
map[method][path]を使用してルーティングテーブルを格納 - 完全一致が優先され、失敗時にはプレフィックス一致で
{id}の抽出を試みる - パスパラメータは
X-Resource-IDリクエストヘッダーを通じてハンドラに渡される
4. ハンドラクロージャ
handleGetTodos(store)などの関数はstore参照をキャプチャするクロージャを返します。これにより、ハンドラはグローバル変数なしで共有ストレージレイヤーにアクセスできます。
5. 統一レスポンス形式
すべてのエンドポイントは同じJSON構造{success, data, error}を返すため、クライアントは一度の解析でリクエストが成功したかどうかを判断できます。
❓ よくある質問
質問1:なぜsync.Mutexではなくsync.RWMutexを使用するのですか?
Mutexは1つのgoroutineしか同時にアクセスできませんが、RWMutexは複数の読み取り操作の同時実行を許可します。書き込み操作のみが排他的にロックします。「読み取りが多い、書き込みが少ない」シナリオ(APIのクエリが変更を大幅に上回る場合)では、RWMutexは並行性能を大幅に向上させます。
質問2:このルーターを本番環境で使用すべきですか?
推奨されません。このルーターは教育目的の簡略化された実装で、正規表現マッチング、ワイルドカード、メソッド自動検出などの高度な機能が欠けています。本番環境ではchi、gorilla/mux、またはGo 1.22+の組み込み強化ルーティングなどの成熟したルーティングライブラリを使用してください。
質問3:インメモリストレージのデータは再起動後に失われます。どう解決しますか?
このレッスンではREST APIの部分に集中するためにインメモリストレージを使用しています。実際のプロジェクトでは、データベース(SQLite、PostgreSQL、MongoDBなど)にデータを永続化する必要があります。次のレッスンではGoでデータベースを操作する方法を学びます。
質問4:ミドルウェアの実行順序は重要ですか?
はい。Recoveryは最も外側にして、すべての内部パニックをキャッチできるようにします。AuthはLoggingの前に配置し、認証されていないリクエストがビジネスログを生成しないようにします(ただし、逆にして認証されていないリクエストを含むすべてのリクエストをログに記録することも可能です)。ビジネスニーズに基づいて柔軟に調整してください。
📖 まとめ
このレッスンでは、以前学んだ複数の知識ポイントを総合的に活用して、ゼロから完全なREST APIを構築しました:
net/httpを使用してHTTPサーバーとカスタムルーターを実装encoding/jsonを使用してJSONシリアライズとデシリアライズを実行sync.RWMutexを使用して並行安全なインメモリストレージを実現- ミドルウェアチェーンパターンを使用して横断的関心事(ログ記録、認証、回復)を処理
- クロージャを使用して依存性注入を実装し、ハンドラが共有リソースにアクセスできるようにした
- RESTful規約に従ってAPIパスとHTTPメソッドを設計
これらのパターンはGo Web開発の基盤です。これらをマスターすれば、データベース統合、JWT認証、APIレート制限などの複雑なシナリオに容易に拡張できます。
📝 演習
演習1:ページネーションサポートを追加
GET /api/todosエンドポイントを変更して、クエリパラメータ?page=1&size=10によるページネーション結果をサポートしてください。ヒント:r.URL.Query()を使用してパラメータを読み取り、GetAll結果をスライスします。
演習2:CORSミドルウェアを追加
以下のヘッダーをレスポンスに追加するCORSMiddlewareを作成してください:
Access-Control-Allow-Origin: *Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers: Content-Type, X-API-Key
OPTIONSプリフライトリクエストは204を直接返すように処理してください。ミドルウェアチェーンに追加してください。
演習3:検索機能を実装
新しいルートGET /api/todos/search?q=keywordを追加して、インメモリストレージ内で(strings.Containsを使用して)タイトルのあいまいマッチングを行い、一致するTodoアイテムを返してください。
次のレッスン:データベース操作



