1. REST API 設計の本質:リソース表現と HTTP メソッド

酒本先輩!前回までは Express のミドルウェアやエラーハンドリングを学びました。
でも、API のエンドポイント(URL)を作るとき、/api/get-cart にするか /api/cart にするか迷うことがあります。実務での正しい決め手ってあるんですか?

すごく良い視点ね!それがまさに REST(Representational State Transfer) の考え方よ。
REST API 設計の基本原則は 『URLは操作(動詞)ではなくリソース(名詞)を表し、操作は HTTP メソッドで表現する』 というルールなの。
ダメな設計(アンチパターン)と RESTful な設計の比較
- ❌
/api/get-cart(GET) … URL にgetという動詞が入っている - ❌
/api/create-item(POST) … URL にcreateという動詞が入っている - ⭕️
/api/cart/items(GET) … カート内の商品一覧を取得 - ⭕️
/api/cart/items(POST) … カートに新しい商品を追加
【リソース(名詞)】 ✕ 【HTTPメソッド(動詞)】 の組み合わせで表現する!
GET /api/cart/items ---> カート一覧の取得 (200 OK)
POST /api/cart/items ---> 商品の新規追加 (201 Created)
PUT /api/cart/items/:id ---> 指定商品の全置換 (200 OK)
PATCH /api/cart/items/:id ---> 指定商品の一部更新 (200 OK)
DELETE /api/cart/items/:id ---> 指定商品の削除 (204 No Content)
2. HTTP ステータスコードの厳密な使い分け

REST API では、処理結果に応じた ステータスコードの厳密な使い分け が必須よ。
『エラーが起きたけどレスポンスボディにエラー内容を入れたから 200 OK を返す』なんて設計は絶対にダメ!」
実務で頻出する主要なステータスコードは以下の通りよ。
| 分類 | コード | 意味 | ユースケース |
|---|---|---|---|
| 2xx 成功 | 200 OK | リクエスト成功 | GET/PUT/PATCH の成功時 |
| 201 Created | リソース作成成功 | POST による新規作成成功時(作成されたオブジェクトを返す) | |
| 204 No Content | 処理成功・返却データなし | DELETE 成功時(レスポンスボディは空) | |
| 4xx クライアントエラー | 400 Bad Request | リクエスト不備 | バリデーションエラー、JSON パースエラー |
| 401 Unauthorized | 未認証 | ログインが必要なエンドポイントへの未認証アクセス | |
| 403 Forbidden | 認可エラー(権限不足) | ログイン済みだが対象リソースへのアクセス権がない | |
| 404 Not Found | リソース未存在 | 存在しない URL や ID へのアクセス | |
| 5xx サーバーエラー | 500 Internal Server Error | サーバー内部エラー | DB 障害、バグによる未補足例外 |
3. 実践コード:RESTful 規約に準拠した EC カート API

それじゃあ、第9回で作成した Express アプリケーションを、RESTful な規約と適切なステータスコードを適用したコードに更新しましょう!
// src/index.ts
import express, { Request, Response, NextFunction } from 'express';
const app = express();
const PORT = 3000;
app.use(express.json());
// 共通ロギングミドルウェア
app.use((req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
const { method, url } = req;
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`[LOG] ${new Date().toISOString()} | ${method} ${url} | Status: ${res.statusCode} | ${duration}ms`);
});
next();
});
// カート内商品の型定義
interface CartItem {
id: number;
productName: string;
price: number;
quantity: number;
}
// 疑似データベース(インメモリ)
let cartItems: CartItem[] = [
{ id: 1, productName: 'キーボード', price: 12000, quantity: 1 }
];
let nextId = 2;
// ==========================================
// 1. GET /api/cart/items (一覧取得 -> 200 OK)
// ==========================================
app.get('/api/cart/items', (req: Request, res: Response) => {
res.status(200).json({
data: cartItems,
total: cartItems.length
});
});
// ==========================================
// 2. POST /api/cart/items (新規追加 -> 201 Created / 400 Bad Request)
// ==========================================
app.post('/api/cart/items', (req: Request, res: Response) => {
const { productName, price, quantity } = req.body;
// バリデーションエラー (400 Bad Request)
if (!productName || typeof price !== 'number' || price <= 0) {
res.status(400).json({
error: 'BAD_REQUEST',
message: 'productName は必須であり、price は正の数値である必要があります。'
});
return;
}
const newItem: CartItem = {
id: nextId++,
productName,
price,
quantity: quantity ?? 1
};
cartItems.push(newItem);
// 201 Created でリソース生成完了を通知
res.status(201).json(newItem);
});
// ==========================================
// 3. PATCH /api/cart/items/:id (一部更新 -> 200 OK / 400 / 404)
// ==========================================
app.patch('/api/cart/items/:id', (req: Request, res: Response) => {
const id = Number(req.params.id);
if (isNaN(id)) {
res.status(400).json({ error: 'BAD_REQUEST', message: 'IDは数値で指定してください。' });
return;
}
const item = cartItems.find(i => i.id === id);
if (!item) {
res.status(404).json({ error: 'NOT_FOUND', message: '指定されたアイテムが見つかりません。' });
return;
}
const { quantity } = req.body;
if (typeof quantity === 'number' && quantity > 0) {
item.quantity = quantity;
}
res.status(200).json(item);
});
// ==========================================
// 4. DELETE /api/cart/items/:id (削除 -> 204 No Content / 404)
// ==========================================
app.delete('/api/cart/items/:id', (req: Request, res: Response) => {
const id = Number(req.params.id);
if (isNaN(id)) {
res.status(400).json({ error: 'BAD_REQUEST', message: 'IDは数値で指定してください。' });
return;
}
const index = cartItems.findIndex(i => i.id === id);
if (index === -1) {
res.status(404).json({ error: 'NOT_FOUND', message: '指定されたアイテムが見つかりません。' });
return;
}
cartItems.splice(index, 1);
// 204 No Content: 削除成功(レスポンスボディは空)
res.status(204).send();
});
// 404 Not Found ハンドラー
app.use((req: Request, res: Response) => {
res.status(404).json({ error: 'NOT_FOUND', message: 'リクエストされたエンドポイントが存在しません。' });
});
// 集中エラーハンドラー (500)
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: 'INTERNAL_SERVER_ERROR', message: '予期せぬエラーが発生しました。' });
});
app.listen(PORT, () => {
console.log(` サーバー起動: http://localhost:${PORT}`);
});
4. curl による動作確認手順

さっそく、ステータスコードとレスポンスボディの挙動を curl -i (ヘッダー情報表示)で確認してみます!
1. 新規追加(POST -> 201 Created)
curl -i -X POST http://localhost:3000/api/cart/items \
-H "Content-Type: application/json" \
-d '{"productName": "エルゴノミクスマウス", "price": 8500, "quantity": 2}'
実行結果:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
{"id":2,"productName":"エルゴノミクスマウス","price":8500,"quantity":2}
2. 不正データでの追加(POST -> 400 Bad Request)
curl -i -X POST http://localhost:3000/api/cart/items \
-H "Content-Type: application/json" \
-d '{"productName": ""}'
実行結果:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{"error":"BAD_REQUEST","message":"productName は必須であり、price は正の数値である必要があります。"}
3. 数量の更新(PATCH -> 200 OK)
curl -i -X PATCH http://localhost:3000/api/cart/items/2 \
-H "Content-Type: application/json" \
-d '{"quantity": 5}'
実行結果:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":2,"productName":"エルゴノミクスマウス","price":8500,"quantity":5}
4. リソースの削除(DELETE -> 204 No Content)
curl -i -X DELETE http://localhost:3000/api/cart/items/2
実行結果:
HTTP/1.1 204 No Content
(※204 No Content のためレスポンスボディは返りません)
5. 削除済みのリソースへアクセス(DELETE -> 404 Not Found)
curl -i -X DELETE http://localhost:3000/api/cart/items/2
実行結果:
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
{"error":"NOT_FOUND","message":"指定されたアイテムが見つかりません。"}

レスポンスヘッダーのステータスコードを見るだけで、処理結果がひと目でわかりますね!
204 No Content の時はボディが空になるのも納得です!
本日のまとめ
- REST API 原則: URL には名詞(リソース)を指定し、操作は HTTP メソッド(GET/POST/PATCH/DELETE)で表現する。
- ステータスコードの厳密な運用:
- 生成成功は
201 Created - 削除成功は
204 No Content - 不正リクエストは
400 Bad Request - 未存在リソースは
404 Not Found - 一貫性のあるエラーレスポンス: エラーコードとメッセージを統一された JSON 形式で返す設計がクライアント開発において重要。
次回:第11回「Zodライブラリによるスキーマバリデーションとランタイム型安全の確立」に続く

