課題18: RESTful API実装

課題概要

この課題では、net/httpパッケージを使って、完全なRESTful APIサーバーを実装します。CRUD操作、バリデーション、エラーハンドリング、ミドルウェアなど、実践的なWeb開発の要素をすべて含みます。

マンダトリー要件(80点)

要件1: タスク管理API(40点)

タスク管理システムのREST APIを実装してください。

実装ファイル: taskapi/main.go

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strconv"
    "strings"
    "sync"
    "time"
)

type Task struct {
    ID          int       `json:"id"`
    Title       string    `json:"title"`
    Description string    `json:"description"`
    Completed   bool      `json:"completed"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`
}

type TaskStore struct {
    mu     sync.RWMutex
    tasks  map[int]Task
    nextID int
}

func NewTaskStore() *TaskStore {
    return &TaskStore{
        tasks:  make(map[int]Task),
        nextID: 1,
    }
}

// TODO: 以下のメソッドを実装

// GetAll はすべてのタスクを返します
func (s *TaskStore) GetAll() []Task {
    // TODO: 実装
}

// GetByID は指定されたIDのタスクを返します
func (s *TaskStore) GetByID(id int) (Task, bool) {
    // TODO: 実装
}

// Create は新しいタスクを作成します
func (s *TaskStore) Create(title, description string) Task {
    // TODO: 実装
}

// Update は既存のタスクを更新します
func (s *TaskStore) Update(id int, title, description string, completed bool) (Task, error) {
    // TODO: 実装
}

// Delete はタスクを削除します
func (s *TaskStore) Delete(id int) error {
    // TODO: 実装
}

// API ハンドラー

type TaskAPI struct {
    store *TaskStore
}

func NewTaskAPI() *TaskAPI {
    return &TaskAPI{
        store: NewTaskStore(),
    }
}

// GET /tasks - すべてのタスクを取得
// POST /tasks - 新しいタスクを作成
func (api *TaskAPI) tasksHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        // TODO: すべてのタスクを返す
    case http.MethodPost:
        // TODO: 新しいタスクを作成
        // リクエストボディからtitle, descriptionを取得
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

// GET /tasks/{id} - 特定のタスクを取得
// PUT /tasks/{id} - タスクを更新
// DELETE /tasks/{id} - タスクを削除
func (api *TaskAPI) taskHandler(w http.ResponseWriter, r *http.Request) {
    // URLからIDを抽出
    parts := strings.Split(r.URL.Path, "/")
    if len(parts) < 3 {
        http.Error(w, "Invalid path", http.StatusBadRequest)
        return
    }

    id, err := strconv.Atoi(parts[2])
    if err != nil {
        http.Error(w, "Invalid task ID", http.StatusBadRequest)
        return
    }

    switch r.Method {
    case http.MethodGet:
        // TODO: 指定されたIDのタスクを返す
    case http.MethodPut:
        // TODO: タスクを更新
    case http.MethodDelete:
        // TODO: タスクを削除
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func main() {
    api := NewTaskAPI()

    http.HandleFunc("/tasks", api.tasksHandler)
    http.HandleFunc("/tasks/", api.taskHandler)

    fmt.Println("Server running on :8080")
    http.ListenAndServe(":8080", nil)
}

要件2: ミドルウェアの実装(20点)

ロギングとCORSミドルウェアを実装してください。

実装ファイル: taskapi/middleware.go

package main

import (
    "log"
    "net/http"
    "time"
)

// loggingMiddleware はリクエストをロギングします
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // TODO: リクエスト情報をログ出力
        // - メソッド
        // - パス
        // - 処理時間

        next(w, r)

        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    }
}

// corsMiddleware はCORSヘッダーを設定します
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // TODO: CORSヘッダーを設定
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

        // OPTIONSリクエストの処理
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return
        }

        next(w, r)
    }
}

// chain は複数のミドルウェアを連結します
func chain(f http.HandlerFunc, middlewares ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
    // TODO: 実装
}

要件3: バリデーションとエラーハンドリング(20点)

入力バリデーションとエラーレスポンスを実装してください。

実装ファイル: taskapi/validation.go

package main

import (
    "encoding/json"
    "net/http"
)

type ErrorResponse struct {
    Error   string `json:"error"`
    Message string `json:"message"`
}

// respondJSON はJSON形式でレスポンスを返します
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

// respondError はエラーレスポンスを返します
func respondError(w http.ResponseWriter, status int, message string) {
    respondJSON(w, status, ErrorResponse{
        Error:   http.StatusText(status),
        Message: message,
    })
}

// validateTask はタスクのバリデーションを行います
func validateTask(title, description string) error {
    // TODO: 実装
    // - titleは必須、1-100文字
    // - descriptionは0-500文字
}

期待される動作

APIエンドポイント

# すべてのタスクを取得
GET /tasks
Response: 200 OK
[
  {
    "id": 1,
    "title": "タスク1",
    "description": "説明1",
    "completed": false,
    "created_at": "2024-01-15T10:00:00Z",
    "updated_at": "2024-01-15T10:00:00Z"
  }
]

# タスクを作成
POST /tasks
Body: {"title": "新しいタスク", "description": "説明"}
Response: 201 Created
{
  "id": 2,
  "title": "新しいタスク",
  "description": "説明",
  "completed": false,
  "created_at": "2024-01-15T10:05:00Z",
  "updated_at": "2024-01-15T10:05:00Z"
}

# 特定のタスクを取得
GET /tasks/1
Response: 200 OK
{...}

# タスクを更新
PUT /tasks/1
Body: {"title": "更新", "description": "更新", "completed": true}
Response: 200 OK
{...}

# タスクを削除
DELETE /tasks/1
Response: 204 No Content

エラーレスポンス

# 存在しないタスク
GET /tasks/999
Response: 404 Not Found
{
  "error": "Not Found",
  "message": "Task not found"
}

# バリデーションエラー
POST /tasks
Body: {"title": "", "description": "説明"}
Response: 400 Bad Request
{
  "error": "Bad Request",
  "message": "Title is required"
}

ボーナス課題(20点)

ボーナス1: 認証ミドルウェア(10点)

APIキーベースの認証を実装してください。

実装ファイル: taskapi/auth.go

package main

import (
    "net/http"
)

const validAPIKey = "secret-api-key"

// authMiddleware はAPI認証を行います
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // TODO: 実装
        // 1. X-API-Keyヘッダーからキーを取得
        // 2. キーを検証
        // 3. 無効な場合は401を返す

        apiKey := r.Header.Get("X-API-Key")

        if apiKey == "" {
            respondError(w, http.StatusUnauthorized, "API key is required")
            return
        }

        if apiKey != validAPIKey {
            respondError(w, http.StatusUnauthorized, "Invalid API key")
            return
        }

        next(w, r)
    }
}

ボーナス2: ページネーション(5点)

タスクリストにページネーションを追加してください。

// GET /tasks?page=1&limit=10

func (api *TaskAPI) tasksHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodGet {
        // クエリパラメータを取得
        query := r.URL.Query()
        page, _ := strconv.Atoi(query.Get("page"))
        limit, _ := strconv.Atoi(query.Get("limit"))

        // デフォルト値
        if page < 1 {
            page = 1
        }
        if limit < 1 || limit > 100 {
            limit = 10
        }

        // TODO: ページネーション処理
        // - 開始インデックスと終了インデックスを計算
        // - 該当範囲のタスクを返す
    }
}

ボーナス3: フィルタリングと検索(5点)

タスクのフィルタリングと検索機能を追加してください。

// GET /tasks?completed=true
// GET /tasks?search=keyword

func (api *TaskAPI) tasksHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodGet {
        query := r.URL.Query()

        // フィルタリング
        completedStr := query.Get("completed")
        searchKeyword := query.Get("search")

        tasks := api.store.GetAll()

        // TODO: フィルタリング処理
        // - completedフラグでフィルタ
        // - searchキーワードでタイトルと説明を検索

        respondJSON(w, http.StatusOK, tasks)
    }
}

テスト方法

curlでのテスト

# タスクを作成
curl -X POST http://localhost:8080/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"買い物","description":"牛乳を買う"}'

# すべてのタスクを取得
curl http://localhost:8080/tasks

# 特定のタスクを取得
curl http://localhost:8080/tasks/1

# タスクを更新
curl -X PUT http://localhost:8080/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"買い物","description":"牛乳とパンを買う","completed":true}'

# タスクを削除
curl -X DELETE http://localhost:8080/tasks/1

# 認証付きリクエスト(ボーナス)
curl http://localhost:8080/tasks \
  -H "X-API-Key: secret-api-key"

評価基準

項目 配点 詳細
タスク管理API 40点 CRUD操作がすべて動作する
ミドルウェア 20点 ロギングとCORSが正しく実装されている
バリデーション 20点 入力検証とエラーレスポンスが適切
**ボーナス1: 認証** 10点 APIキー認証が実装されている
**ボーナス2: ページネーション** 5点 ページング機能が動作する
**ボーナス3: フィルタリング** 5点 検索とフィルタが実装されている

提出方法

以下のディレクトリ構造で提出してください:

submission/
├── taskapi/
│   ├── main.go
│   ├── middleware.go
│   ├── validation.go
│   └── auth.go           # ボーナス課題
├── README.md             # API仕様とテスト方法
└── test.sh               # テストスクリプト(オプション)

ヒント