Engineer Quiz

← 記事一覧

HTTPメソッドとRESTful API設計

HTTPメソッドとRESTful API設計

📚 概要

HTTP(Hypertext Transfer Protocol)は、Webの基盤となる通信プロトコルです。RESTful APIは、HTTPメソッドを活用してリソース指向のAPIを設計する手法です。

この記事では、HTTPメソッドの種類、RESTful APIの設計原則、ベストプラクティスまで詳しく解説します。

🕰️ 歴史的背景

HTTPの誕生 - 1991年

  • 開発者: Tim Berners-Lee(CERN)
  • HTTP/0.9: 最初のバージョン(GETのみ)
  • HTTP/1.0: 1996年、メソッドとヘッダーを追加
  • HTTP/1.1: 1997年、現在も広く使われている
  • HTTP/2: 2015年、パフォーマンス改善
  • HTTP/3: 2022年、QUIC上で動作

RESTの提唱 - 2000年

  • 提唱者: Roy Fielding(博士論文)
  • REST: Representational State Transfer
  • 原則: リソース指向、ステートレス、統一インターフェース
  • 普及: 2010年代にWeb APIの標準となる

🔧 技術解説

HTTPメソッドの種類

graph TB
    A[HTTP Methods] --> B[Safe Methods]
    A --> C[Unsafe Methods]
    
    B --> D[GET]
    B --> E[HEAD]
    B --> F[OPTIONS]
    
    C --> G[POST]
    C --> H[PUT]
    C --> I[PATCH]
    C --> J[DELETE]
    
    style D fill:#51cf66
    style G fill:#4dabf7
    style H fill:#ffd43b
    style J fill:#ff6b6b

1. GET - リソースの取得

特徴:

  • Safe(安全)- リソースを変更しない
  • Idempotent(べき等)- 何度実行しても同じ結果

使用例:

// ユーザー一覧取得
GET /api/users
Response: [
  { "id": 1, "name": "Alice" },
  { "id": 2, "name": "Bob" }
]

// 特定ユーザー取得
GET /api/users/1
Response: { "id": 1, "name": "Alice", "email": "alice@example.com" }

// クエリパラメータでフィルタ
GET /api/users?role=admin&active=true
Response: [
  { "id": 1, "name": "Alice", "role": "admin" }
]

実装例(Express):

app.get('/api/users', async (req, res) => {
  const { role, active } = req.query;
  
  let query = db.select().from(users);
  
  if (role) {
    query = query.where(eq(users.role, role));
  }
  
  if (active === 'true') {
    query = query.where(eq(users.active, true));
  }
  
  const results = await query;
  res.json(results);
});

app.get('/api/users/:id', async (req, res) => {
  const { id } = req.params;
  const user = await db.select().from(users).where(eq(users.id, id)).limit(1);
  
  if (!user[0]) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.json(user[0]);
});

2. POST - リソースの作成

特徴:

  • Unsafe(非安全)- リソースを変更する
  • Non-idempotent(非べき等)- 実行のたびに新しいリソースが作られる

使用例:

// 新規ユーザー作成
POST /api/users
Body: { "name": "Charlie", "email": "charlie@example.com" }
Response: { "id": 3, "name": "Charlie", "email": "charlie@example.com" }
Status: 201 Created
Location: /api/users/3

実装例:

app.post('/api/users', async (req, res) => {
  const { name, email } = req.body;
  
  // バリデーション
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }
  
  // 重複チェック
  const existing = await db.select().from(users).where(eq(users.email, email)).limit(1);
  if (existing[0]) {
    return res.status(409).json({ error: 'Email already exists' });
  }
  
  // 作成
  const [user] = await db.insert(users).values({ name, email }).returning();
  
  res.status(201)
     .location(`/api/users/${user.id}`)
     .json(user);
});

3. PUT - リソースの完全更新

特徴:

  • Unsafe(非安全)
  • Idempotent(べき等)- 何度実行しても同じ結果

使用例:

// ユーザー情報の完全更新
PUT /api/users/1
Body: { "name": "Alice Smith", "email": "alice.smith@example.com", "role": "admin" }
Response: { "id": 1, "name": "Alice Smith", "email": "alice.smith@example.com", "role": "admin" }
Status: 200 OK

実装例:

app.put('/api/users/:id', async (req, res) => {
  const { id } = req.params;
  const { name, email, role } = req.body;
  
  // すべてのフィールドが必要
  if (!name || !email || !role) {
    return res.status(400).json({ error: 'All fields are required' });
  }
  
  const [user] = await db.update(users)
    .set({ name, email, role })
    .where(eq(users.id, id))
    .returning();
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.json(user);
});

4. PATCH - リソースの部分更新

特徴:

  • Unsafe(非安全)
  • Not necessarily idempotent(必ずしもべき等ではない)

使用例:

// ユーザー名のみ更新
PATCH /api/users/1
Body: { "name": "Alice Updated" }
Response: { "id": 1, "name": "Alice Updated", "email": "alice@example.com", "role": "user" }
Status: 200 OK

実装例:

app.patch('/api/users/:id', async (req, res) => {
  const { id } = req.params;
  const updates = req.body;
  
  // 許可されたフィールドのみ
  const allowedFields = ['name', 'email', 'role'];
  const filteredUpdates = {};
  
  for (const key of Object.keys(updates)) {
    if (allowedFields.includes(key)) {
      filteredUpdates[key] = updates[key];
    }
  }
  
  if (Object.keys(filteredUpdates).length === 0) {
    return res.status(400).json({ error: 'No valid fields to update' });
  }
  
  const [user] = await db.update(users)
    .set(filteredUpdates)
    .where(eq(users.id, id))
    .returning();
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.json(user);
});

5. DELETE - リソースの削除

特徴:

  • Unsafe(非安全)
  • Idempotent(べき等)- 何度削除しても結果は同じ

使用例:

// ユーザー削除
DELETE /api/users/1
Response: { "message": "User deleted successfully" }
Status: 204 No Content または 200 OK

実装例:

app.delete('/api/users/:id', async (req, res) => {
  const { id } = req.params;
  
  const result = await db.delete(users).where(eq(users.id, id)).returning();
  
  if (result.length === 0) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.status(204).send(); // No Content
  // または
  // res.json({ message: 'User deleted successfully' });
});

💡 RESTful API設計原則

1. リソース指向

URLはリソースを表現し、動詞ではなく名詞を使う。

// ❌ 悪い例: 動詞を使う
GET /getUsers
POST /createUser
DELETE /deleteUser/1

// ✅ 良い例: 名詞(リソース)+ HTTPメソッド
GET /users
POST /users
DELETE /users/1

2. ステートレス

各リクエストは独立しており、サーバーはセッション状態を保持しない。

// ✅ ステートレス: トークンをリクエストに含める
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// ❌ ステートフル: サーバーのセッションに依存
GET /api/users
Cookie: session_id=abc123

3. 階層構造

リソース間の関係を URL で表現する。

// ユーザーの投稿一覧
GET /api/users/1/posts

// 特定の投稿
GET /api/users/1/posts/42

// 投稿のコメント
GET /api/posts/42/comments

// 特定のコメント
GET /api/posts/42/comments/7

実装例:

// ユーザーの投稿一覧
app.get('/api/users/:userId/posts', async (req, res) => {
  const { userId } = req.params;
  
  const userPosts = await db.select()
    .from(posts)
    .where(eq(posts.userId, userId));

  res.json(userPosts);
});

// 投稿のコメント一覧
app.get('/api/posts/:postId/comments', async (req, res) => {
  const { postId } = req.params;
  
  const postComments = await db.select()
    .from(comments)
    .where(eq(comments.postId, postId));

  res.json(postComments);
});

4. べき等性の重要性

graph LR
    A[HTTP Methods] --> B{Idempotent?}
    B -->|Yes| C[GET PUT DELETE]
    B -->|No| D[POST]
    
    C --> E[Safe to Retry]
    D --> F[Avoid Auto-Retry]
    
    style C fill:#51cf66
    style D fill:#ff6b6b

べき等性の例:

// ✅ べき等: 何度実行しても同じ結果
PUT /api/users/1
Body: { "name": "Alice", "email": "alice@example.com" }
// 1回実行も10回実行も結果は同じ

// ❌ 非べき等: 実行のたびに新しいリソース
POST /api/users
Body: { "name": "Alice", "email": "alice@example.com" }
// 実行のたびに新しいユーザーが作られる

📊 HTTPステータスコード

よく使うステータスコード

コード 意味 使用例
200 OK リクエスト成功
201 Created リソース作成成功
204 No Content 成功(レスポンスボディなし)
400 Bad Request リクエストが不正
401 Unauthorized 認証が必要
403 Forbidden 権限がない
404 Not Found リソースが見つからない
409 Conflict リソースの競合
422 Unprocessable Entity バリデーションエラー
500 Internal Server Error サーバーエラー

実装例:

app.post('/api/users', async (req, res) => {
  const { name, email } = req.body;
  
  // 400: バリデーションエラー
  if (!name || !email) {
    return res.status(400).json({ 
      error: 'Validation failed',
      details: { name: 'required', email: 'required' }
    });
  }
  
  // 409: 既に存在
  const existing = await db.select().from(users).where(eq(users.email, email)).limit(1);
  if (existing[0]) {
    return res.status(409).json({ error: 'Email already exists' });
  }
  
  try {
    const [user] = await db.insert(users).values({ name, email }).returning();
    
    // 201: 作成成功
    res.status(201)
       .location(`/api/users/${user.id}`)
       .json(user);
  } catch (error) {
    // 500: サーバーエラー
    console.error(error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

🎯 ベストプラクティス

1. バージョニング

// URL パス
GET /api/v1/users
GET /api/v2/users

// ヘッダー
GET /api/users
Accept: application/vnd.myapi.v1+json

2. ペジネーション

GET /api/users?page=2&limit=20

app.get('/api/users', async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 20;
  const offset = (page - 1) * limit;
  
  const userList = await db.select()
    .from(users)
    .limit(limit)
    .offset(offset);

  const total = await db.select({ count: count() }).from(users);
  
  res.json({
    data: users,
    pagination: {
      page,
      limit,
      total: total[0].count,
      totalPages: Math.ceil(total[0].count / limit)
    }
  });
});

3. フィルタリング・ソート

GET /api/users?role=admin&sort=-createdAt&search=alice

app.get('/api/users', async (req, res) => {
  const { role, sort, search } = req.query;
  
  let query = db.select().from(users);
  
  // フィルタ
  if (role) query = query.where(eq(users.role, role));
  if (search) query = query.where(like(users.name, `%${search}%`));
  
  // ソート
  if (sort) {
    const direction = sort.startsWith('-') ? desc : asc;
    const field = sort.replace(/^-/, '');
    query = query.orderBy(direction(users[field]));
  }
  
  const results = await query;
  res.json(results);
});

4. エラーレスポンスの統一

interface ErrorResponse {
  error: string;
  message: string;
  details?: any;
  code?: string;
}

app.use((err, req, res, next) => {
  const response: ErrorResponse = {
    error: err.name || 'Error',
    message: err.message,
    code: err.code
  };
  
  if (process.env.NODE_ENV === 'development') {
    response.details = err.stack;
  }
  
  res.status(err.statusCode || 500).json(response);
});

🔍 関連する問題

この記事に関連するクイズ問題:

  • Q2: HTTPメソッドの種類と使い分け
  • Q10: RESTful APIの設計原則

📝 まとめ

  • GET: リソース取得(Safe, Idempotent)
  • POST: リソース作成(Non-idempotent)
  • PUT: 完全更新(Idempotent)
  • PATCH: 部分更新(Not necessarily idempotent)
  • DELETE: 削除(Idempotent)
  • REST原則: リソース指向、ステートレス、階層構造
  • ベストプラクティス: バージョニング、ペジネーション、エラーハンドリング

次のステップ: 実際にRESTful APIを設計・実装してみましょう!

推奨リソース: