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:#ff6b6b1. 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を設計・実装してみましょう!
推奨リソース: