課題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 # テストスクリプト(オプション)
ヒント
- sync.RWMutex: 読み込みと書き込みのロックを分離
- json.Decoder: リクエストボディの解析
- json.Encoder: レスポンスのエンコード
- time.Now(): タイムスタンプの生成
- strings.Split: URLパスからIDを抽出
- net/http Package
- Writing Web Applications
- RESTful API Design
- HTTP Status Codes