MongoDB: RESTful API設計:CRUD操作とバリデーション

最終更新:2026-08-26

RESTful APIはモダンWebアプリケーションの基盤—マスターすれば保守性の高いAPIサービスを構築できます。

1. 学習内容


100%
graph LR
    Client[クライアント] -->|GET /api/products| List[商品一覧<br/>ページネーション]
    Client -->|GET /api/products/:id| Detail[商品詳細]
    Client -->|POST /api/products| Create[商品作成<br/>管理者のみ]
    Client -->|PUT /api/products/:id| Update[商品更新<br/>管理者のみ]
    Client -->|DELETE /api/products/:id| Delete[商品削除<br/>管理者のみ]

    List -->|200 OK| Client
    Detail -->|200 OK / 404| Client
    Create -->|201 Created| Client
    Update -->|200 OK| Client
    Delete -->|204 No Content| Client

    style Create fill:#d4edda
    style Update fill:#cce5ff
    style Delete fill:#fff3cd

2. RESTful API設計原則

概念概要: RESTful APIはHTTPプロトコルに基づいたリソース中心のAPI設計スタイルです。各URLがリソースを表し、HTTPメソッド(GET/POST/PUT/DELETE)がリソースへの操作を表します。統一的な設計規約に従うことで、APIは直感的に理解可能で、保守しやすく、拡張しやすくなります。

RESTful設計の核心原則:

原則 説明
リソース中心 URLは名詞(リソース)で表現 /products, /orders
HTTPメソッド 操作はHTTPメソッドで表現 GET=読み取り、POST=作成
ステートレス 各リクエストは独立 セッション状態を持たない
一貫性 統一的なレスポンス形式 { success, data, meta }
バージョニング APIバージョンを明示 /api/v1/products
HATEOAS ハイパーメディアリンク 次ページURLをレスポンスに含む

HTTPメソッドとCRUDの対応:

HTTPメソッド CRUD操作 意味 成功ステータス 失敗ステータス
GET Read リソース取得 200 OK 404 Not Found
POST Create リソース作成 201 Created 400 Bad Request
PUT Update リソース全体更新 200 OK 400/404
PATCH Update リソース部分更新 200 OK 400/404
DELETE Delete リソース削除 204 No Content 404
JAVASCRIPT
// === RESTful URL設計例 ===
// 商品リソース
GET    /api/v1/products           // 商品一覧(ページネーション付き)
GET    /api/v1/products/:id       // 商品詳細
POST   /api/v1/products           // 商品作成
PUT    /api/v1/products/:id       // 商品全体更新
PATCH  /api/v1/products/:id       // 商品部分更新
DELETE /api/v1/products/:id       // 商品削除

// ネストしたリソース(商品に対するレビュー)
GET    /api/v1/products/:id/reviews      // 商品のレビュー一覧
POST   /api/v1/products/:id/reviews      // レビュー作成
GET    /api/v1/reviews/:id               // レビュー詳細
DELETE /api/v1/reviews/:id               // レビュー削除


3. CRUD操作の標準パターン

概念説明: CRUD操作はRESTful APIの基本機能です。一貫したパターンに従うことで、コードの可読性と保守性が向上します。標準パターンには:エラー処理、成功レスポンス形式、データ検証、ページネーション処理が含まれます。

標準レスポンス形式:

JAVASCRIPT
// 成功レスポンス(一覧)
{
  "success": true,
  "data": [...],
  "meta": { "page": 1, "limit": 20, "total": 100, "pages": 5 }
}

// 成功レスポンス(詳細・作成・更新)
{
  "success": true,
  "data": { ... }
}

// エラーレスポンス
{
  "success": false,
  "error": "エラーメッセージ",
  "details": [...]  // 任意
}

(1) 商品一覧API(ページネーション + 検索 + フィルタ)

JAVASCRIPT
// controllers/productController.js
exports.listProducts = async (req, res) => {
  const {
    page = 1,
    limit = 20,
    category,
    minPrice,
    maxPrice,
    search,
    sort = 'createdAt',
    order = 'desc'
  } = req.query;

  // クエリ条件を構築
  const query = { isActive: true };

  if (category) query.category = category;
  if (minPrice || maxPrice) {
    query.price = {};
    if (minPrice) query.price.$gte = +minPrice;
    if (maxPrice) query.price.$lte = +maxPrice;
  }
  if (search) {
    query.$or = [
      { title: { $regex: search, $options: 'i' } },
      { description: { $regex: search, $options: 'i' } }
    ];
  }

  // 並び替え条件
  const sortOption = {};
  sortOption[sort] = order === 'desc' ? -1 : 1;

  // 並列実行(find + count)
  const [products, total] = await Promise.all([
    Product.find(query)
      .select('sku title price thumbnail rating category')
      .sort(sortOption)
      .limit(+limit)
      .skip((+page - 1) * +limit)
      .lean(),
    Product.countDocuments(query)
  ]);

  res.json({
    success: true,
    data: products,
    meta: {
      page: +page,
      limit: +limit,
      total,
      pages: Math.ceil(total / +limit)
    }
  });
};

(2) 商品詳細API

JAVASCRIPT
exports.getProduct = async (req, res) => {
  const { id } = req.params;

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(400).json({ success: false, error: '無効なID形式です' });
  }

  const product = await Product.findById(id);
  if (!product || !product.isActive) {
    return res.status(404).json({ success: false, error: '商品が見つかりません' });
  }

  res.json({ success: true, data: product });
};

(3) 商品作成API

JAVASCRIPT
exports.createProduct = async (req, res) => {
  const { sku, title, price, category, description, stock } = req.body;

  // 重複チェック
  const existing = await Product.findOne({ sku });
  if (existing) {
    return res.status(409).json({
      success: false,
      error: `SKU ${sku}は既に存在します`
    });
  }

  const product = await Product.create({
    sku,
    title,
    price,
    category,
    description,
    stock
  });

  res.status(201).json({ success: true, data: product });
};

(4) 商品更新API

JAVASCRIPT
exports.updateProduct = async (req, res) => {
  const { id } = req.params;
  const updates = req.body;

  // 更新禁止フィールドを除外
  delete updates._id;
  delete updates.createdAt;
  delete updates.__v;

  const product = await Product.findByIdAndUpdate(
    id,
    { $set: updates },
    {
      new: true,           // 更新後のドキュメントを返す
      runValidators: true  // スキーマ検証を実行
    }
  );

  if (!product) {
    return res.status(404).json({ success: false, error: '商品が見つかりません' });
  }

  res.json({ success: true, data: product });
};

(5) 商品削除API(ソフト削除)

JAVASCRIPT
exports.deleteProduct = async (req, res) => {
  const { id } = req.params;

  // ソフト削除(isDeletedフラグを設定)
  const product = await Product.findByIdAndUpdate(
    id,
    { $set: { isDeleted: true } },
    { new: true }
  );

  if (!product) {
    return res.status(404).json({ success: false, error: '商品が見つかりません' });
  }

  res.status(204).send();  // 204 No Content
};


4. 入力バリデーション

概念説明: 入力バリデーションはAPIセキュリティの第一防衛線です。無効データがアプリケーションに入るのを防ぎ、データ整合性を保証し、セキュリティ脆弱性(SQLインジェクション、NoSQLインジェクションなど)を防止します。JoiはNode.jsで最も広く使用されているバリデーションライブラリです。

バリデーションのベストプラクティス:

レイヤー 責務 ツール
ルート層 リクエスト形式チェック Joi / express-validator
コントローラー層 ビジネスルールチェック カスタムロジック
モデル層 データ整合性チェック Mongooseスキーマ
JAVASCRIPT
// validators/schemas.js
const Joi = require('joi');

// 商品作成・更新スキーマ
const productSchema = Joi.object({
  sku: Joi.string()
    .alphanum()
    .min(3)
    .max(20)
    .required()
    .messages({
      'string.base': 'SKUは文字列である必要があります',
      'string.min': 'SKUは{#limit}文字以上である必要があります',
      'string.max': 'SKUは{#limit}文字以下である必要があります',
      'any.required': 'SKUは必須です'
    }),

  title: Joi.string()
    .min(2)
    .max(200)
    .required(),

  price: Joi.number()
    .positive()
    .precision(2)
    .required(),

  category: Joi.string()
    .valid('Electronics', 'Books', 'Clothing', 'Home')
    .required(),

  description: Joi.string()
    .max(5000)
    .optional(),

  stock: Joi.number()
    .integer()
    .min(0)
    .default(0)
});

// ユーザー登録スキーマ
const registerSchema = Joi.object({
  email: Joi.string()
    .email()
    .required(),

  username: Joi.string()
    .alphanum()
    .min(3)
    .max(30)
    .required(),

  password: Joi.string()
    .min(8)
    .max(100)
    .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .messages({
      'string.pattern.base': 'パスワードは大文字、小文字、数字を含む必要があります'
    })
});

module.exports = { productSchema, registerSchema };
JAVASCRIPT
// middlewares/validate.js
const { productSchema } = require('../validators/schemas');

exports.validateProduct = (req, res, next) => {
  const { error, value } = productSchema.validate(req.body, {
    abortEarly: false,  // 全エラーを返す
    stripUnknown: true  // 不明フィールドを削除
  });

  if (error) {
    const messages = error.details.map(d => d.message);
    return res.status(400).json({
      success: false,
      error: '入力値が無効です',
      details: messages
    });
  }

  req.body = value;  // 検証済み値に置換
  next();
};


5. ページネーション、検索、フィルタリング

概念説明: 大規模データセットのAPIでは、ページネーションは不可欠です。全データを一度に返すと、ネットワーク帯域を浪費し、クライアントのメモリを圧迫し、レスポンス時間が長くなります。ページネーションには「オフセットベース」と「カーソルベース」の2つのアプローチがあります。

ページネーション方式の比較:

方式 実装 利点 欠点 適用
オフセットベース skip((page-1) * limit) ページ番号でジャンプ可能 データ量増加で遅くなる 一般的なWebアプリ
カーソルベース find({_id: {$gt: lastId}}) 高速、データ量に依存しない ページ番号でジャンプ不可 無限スクロール

オフセットベースページネーション:

JAVASCRIPT
// GET /api/products?page=2&limit=20
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 20, 100);  // 最大100件

const products = await Product.find({ isActive: true })
  .skip((page - 1) * limit)
  .limit(limit)
  .lean();

const total = await Product.countDocuments({ isActive: true });

res.json({
  success: true,
  data: products,
  meta: {
    page,
    limit,
    total,
    pages: Math.ceil(total / limit),
    hasNext: page < Math.ceil(total / limit),
    hasPrev: page > 1
  }
});

検索とフィルタリング:

JAVASCRIPT
// GET /api/products?category=Electronics&minPrice=100&maxPrice=500&search=phone

const buildQuery = (filters) => {
  const query = { isActive: true };

  // カテゴリフィルタ
  if (filters.category) {
    query.category = filters.category;
  }

  // 価格範囲フィルタ
  if (filters.minPrice || filters.maxPrice) {
    query.price = {};
    if (filters.minPrice) query.price.$gte = +filters.minPrice;
    if (filters.maxPrice) query.price.$lte = +filters.maxPrice;
  }

  // キーワード検索(インデックス使用)
  if (filters.search) {
    query.$text = { $search: filters.search };
  }

  return query;
};

const query = buildQuery(req.query);
const products = await Product.find(query)
  .sort({ score: { $meta: 'textScore' } })  // テキスト検索スコアでソート
  .lean();


6. 権限制御

概念説明: 権限制御はAPIセキュリティの中核です。RBAC(Role-Based Access Control)は「ユーザー → ロール → 権限」のモデルで、柔軟で管理しやすい権限システムを構築できます。各APIエンドポイントに権限要件を定義し、ミドルウェアで検証します。

RBACモデル:

100%
graph LR
    User[ユーザー] -->|持つ| Role[ロール]
    Role -->|持つ| Permission[権限]
    Permission -->|許可| Resource[リソース<br/>/api/products]

    subgraph "ロール定義"
        R1[customer] --> P1[レビュー作成]
        R2[moderator] --> P2[レビュー管理]
        R3[admin] --> P3[商品管理]
    end

    style User fill:#cce5ff
    style Role fill:#fff3cd
    style Permission fill:#d4edda
ロール 権限 アクセス範囲
customer 商品閲覧、レビュー作成 /products, /reviews(自分)
moderator レビュー管理 /reviews(全て)
admin 商品・ユーザー管理 /products, /users, /reviews(全て)
JAVASCRIPT
// middlewares/auth.js
const jwt = require('jsonwebtoken');

// JWT認証ミドルウェア
exports.authenticate = (req, res, next) => {
  const token = req.header('Authorization')?.replace('Bearer ', '');

  if (!token) {
    return res.status(401).json({ error: '認証が必要です' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;  // { id, role, username }
    next();
  } catch (err) {
    return res.status(401).json({ error: '無効なトークンです' });
  }
};

// 権限認可ミドルウェア
exports.authorize = (...roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: '認証が必要です' });
    }

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'この操作を行う権限がありません' });
    }

    next();
  };
};

// リソース所有者チェック
exports.checkOwnership = (resourceType) => {
  return async (req, res, next) => {
    const resourceId = req.params.id;
    const userId = req.user.id;

    let resource;
    if (resourceType === 'review') {
      resource = await Review.findById(resourceId);
    }

    if (!resource || resource.userId.toString() !== userId) {
      return res.status(403).json({ error: 'このリソースへのアクセス権がありません' });
    }

    next();
  };
};

ルーティングでの使用例:

JAVASCRIPT
// routes/products.js
const { authenticate, authorize } = require('../middlewares/auth');

// 一般ユーザー:閲覧のみ
router.get('/', productController.listProducts);
router.get('/:id', productController.getProduct);

// 管理者:作成・更新・削除
router.post('/',
  authenticate,
  authorize('admin'),
  productController.createProduct
);

router.put('/:id',
  authenticate,
  authorize('admin'),
  productController.updateProduct
);

router.delete('/:id',
  authenticate,
  authorize('admin'),
  productController.deleteProduct
);


7. APIテスト

概念概要: APIテストは品質保証の重要な部分です。Postman、curl、またはテストスクリプトでAPIをテストし、機能が正しく動作することを確認します。テストは正常系(成功ケース)と異常系(エラーケース)の両方をカバーすべきです。

curlテストコマンド集:

BASH
# === 商品一覧取得 ===
curl http://localhost:3000/api/v1/products

# === ページネーション・フィルタ付き ===
curl 'http://localhost:3000/api/v1/products?page=1&limit=10&category=Electronics&minPrice=100'

# === 商品詳細取得 ===
curl http://localhost:3000/api/v1/products/647f1f77bcf86cd799439001

# === 商品作成(管理者のみ)===
curl -X POST http://localhost:3000/api/v1/products \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "sku": "PHONE-001",
    "title": "Smartphone X",
    "price": 599,
    "category": "Electronics",
    "stock": 50
  }'

# === 商品更新 ===
curl -X PUT http://localhost:3000/api/v1/products/647f1f77bcf86cd799439001 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"price": 549, "stock": 45}'

# === 商品削除 ===
curl -X DELETE http://localhost:3000/api/v1/products/647f1f77bcf86cd799439001 \
  -H "Authorization: Bearer <token>"

▶ サンプル:完全な商品CRUD API + ページネーション + バリデーションのハンズオンガイド

JAVASCRIPT
// === 1. モデル定義(models/Product.js)===
const mongoose = require('mongoose');

const ProductSchema = new mongoose.Schema({
  sku: { type: String, required: true, unique: true },
  title: { type: String, required: true, maxlength: 200 },
  description: { type: String, maxlength: 5000 },
  price: { type: Number, required: true, min: 0 },
  category: {
    type: String,
    enum: ['Electronics', 'Books', 'Clothing', 'Home'],
    required: true
  },
  stock: { type: Number, default: 0, min: 0 },
  rating: { type: Number, default: 0, min: 0, max: 5 },
  reviewCount: { type: Number, default: 0 },
  isActive: { type: Boolean, default: true },
  isDeleted: { type: Boolean, default: false }
}, { timestamps: true });

// インデックス
ProductSchema.index({ category: 1, price: -1 });
ProductSchema.index({ title: 'text', description: 'text' });

// ソフト削除フィルタ
ProductSchema.pre(/^find/, function(next) {
  this.where({ isDeleted: { $ne: true } });
  next();
});

module.exports = mongoose.model('Product', ProductSchema);

// === 2. バリデーションスキーマ(validators/schemas.js)===
const Joi = require('joi');

const productSchema = Joi.object({
  sku: Joi.string().alphanum().min(3).max(20).required(),
  title: Joi.string().min(2).max(200).required(),
  price: Joi.number().positive().precision(2).required(),
  category: Joi.string().valid('Electronics', 'Books', 'Clothing', 'Home').required(),
  description: Joi.string().max(5000).optional(),
  stock: Joi.number().integer().min(0).default(0)
});

module.exports = { productSchema };

// === 3. コントローラー(controllers/productController.js)===
const Product = require('../models/Product');
const mongoose = require('mongoose');

exports.listProducts = async (req, res) => {
  const { page = 1, limit = 20, category, search, sort = 'createdAt', order = 'desc' } = req.query;

  const query = { isActive: true };
  if (category) query.category = category;
  if (search) query.$text = { $search: search };

  const sortOption = {};
  sortOption[sort] = order === 'desc' ? -1 : 1;

  const [products, total] = await Promise.all([
    Product.find(query)
      .select('sku title price thumbnail rating category')
      .sort(sortOption)
      .limit(+limit)
      .skip((+page - 1) * +limit)
      .lean(),
    Product.countDocuments(query)
  ]);

  res.json({
    success: true,
    data: products,
    meta: { page: +page, limit: +limit, total, pages: Math.ceil(total / +limit) }
  });
};

exports.createProduct = async (req, res) => {
  const product = await Product.create(req.body);
  res.status(201).json({ success: true, data: product });
};

// === 4. ルーティング(routes/products.js)===
const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');
const { authenticate, authorize } = require('../middlewares/auth');
const { validateProduct } = require('../middlewares/validate');

router.get('/', productController.listProducts);
router.get('/:id', productController.getProduct);

router.post('/',
  authenticate,
  authorize('admin'),
  validateProduct,
  productController.createProduct
);

module.exports = router;

// === 5. テスト ===
// 商品一覧
curl http://localhost:3000/api/v1/products?category=Electronics&page=1&limit=10

// 商品作成
curl -X POST http://localhost:3000/api/v1/products \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{"sku":"PHONE-001","title":"Smartphone X","price":599,"category":"Electronics","stock":50}'

出力:

TEXT 📖 参照専用
RESTful APIが商品一覧(ページネーション・フィルタ付き)と商品作成(管理者のみ・バリデーション付き)を提供。

▶ サンプル 2:Joiバリデーションの実装(難易度 ⭐⭐)

JAVASCRIPT
// validators/schemas.js - Joiバリデーションスキーマ
const Joi = require('joi');

// 商品バリデーションスキーマ
const productSchema = Joi.object({
  sku: Joi.string()
    .alphanum()
    .min(3)
    .max(20)
    .required()
    .messages({
      'string.base': 'SKUは文字列である必要があります',
      'string.min': 'SKUは{#limit}文字以上である必要があります',
      'any.required': 'SKUは必須です'
    }),

  title: Joi.string()
    .min(2)
    .max(200)
    .required()
    .messages({
      'string.min': 'タイトルは{#limit}文字以上必要です'
    }),

  price: Joi.number()
    .positive()
    .precision(2)
    .required(),

  category: Joi.string()
    .valid('Electronics', 'Books', 'Clothing', 'Home', 'Sports')
    .required(),

  description: Joi.string()
    .max(5000)
    .optional()
    .allow(''),

  stock: Joi.number()
    .integer()
    .min(0)
    .default(0)
});

// ユーザー登録バリデーションスキーマ
const registerSchema = Joi.object({
  email: Joi.string()
    .email()
    .required()
    .messages({
      'string.email': '有効なメールアドレスを入力してください'
    }),

  username: Joi.string()
    .alphanum()
    .min(3)
    .max(30)
    .required(),

  password: Joi.string()
    .min(8)
    .max(100)
    .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .messages({
      'string.pattern.base': 'パスワードは大文字、小文字、数字を含む必要があります',
      'string.min': 'パスワードは{#limit}文字以上必要です'
    })
});

module.exports = { productSchema, registerSchema };

// middlewares/validate.js - バリデーションミドルウェア
const { productSchema } = require('../validators/schemas');

exports.validateProduct = (req, res, next) => {
  const { error, value } = productSchema.validate(req.body, {
    abortEarly: false,  // 全エラーを返す
    stripUnknown: true,  // 不明フィールドを削除
    convert: true        // 型変換を許可
  });

  if (error) {
    const messages = error.details.map(d => d.message);
    return res.status(400).json({
      success: false,
      error: '入力値が無効です',
      details: messages
    });
  }

  req.body = value;  // 検証済み値に置換
  next();
};

// テスト
const { error } = productSchema.validate({
  sku: 'AB',
  price: -10,
  category: 'Invalid'
});
// error.details = [
//   { message: 'SKUは3文字以上である必要があります' },
//   { message: '"price"は正の数値である必要があります' },
//   { message: '"category"はElectronics, Books, ...のいずれかである必要があります' }
// ]

出力:

TEXT 📖 参照専用
Joiバリデーション:SKU/タイトル/価格/カテゴリの形式チェック。abortEarly: falseで全エラーを一度に返し、ユーザーフレンドリーなエラーメッセージを表示。

▶ サンプル 3:RBAC権限制御の実装(難易度 ⭐⭐⭐)

JAVASCRIPT
// middlewares/auth.js - RBAC権限制御
const jwt = require('jsonwebtoken');

// JWT認証ミドルウェア
exports.authenticate = (req, res, next) => {
  const authHeader = req.header('Authorization');

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ success: false, error: '認証が必要です' });
  }

  const token = authHeader.replace('Bearer ', '');

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;  // { id, role, username }
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ success: false, error: 'トークンの有効期限が切れています' });
    }
    return res.status(401).json({ success: false, error: '無効なトークンです' });
  }
};

// ロールベース権限チェック
exports.authorize = (...roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ success: false, error: '認証が必要です' });
    }

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        error: 'この操作を行う権限がありません',
        required: roles,
        current: req.user.role
      });
    }

    next();
  };
};

// リソース所有者チェック(レビューなど)
exports.checkOwnership = (resourceType) => {
  return async (req, res, next) => {
    const resourceId = req.params.id;
    const userId = req.user.id;

    try {
      let resource;
      if (resourceType === 'review') {
        const Review = require('../models/Review');
        resource = await Review.findById(resourceId);
      } else if (resourceType === 'order') {
        const Order = require('../models/Order');
        resource = await Order.findById(resourceId);
      }

      if (!resource) {
        return res.status(404).json({ success: false, error: 'リソースが見つかりません' });
      }

      // 管理者は常にアクセス可能
      if (req.user.role === 'admin') {
        return next();
      }

      // 所有者チェック
      if (resource.userId.toString() !== userId) {
        return res.status(403).json({ success: false, error: 'このリソースへのアクセス権がありません' });
      }

      next();
    } catch (error) {
      next(error);
    }
  };
};

// routes/reviews.js - レビュールーティングでの使用例
const express = require('express');
const router = express.Router();
const reviewController = require('../controllers/reviewController');
const { authenticate, authorize, checkOwnership } = require('../middlewares/auth');

// 公開ルート
router.get('/product/:productId', reviewController.listByProduct);

// 認証必要ルート
router.post('/',
  authenticate,
  authorize('customer', 'admin'),
  reviewController.createReview
);

// 所有者または管理者のみ更新可能
router.put('/:id',
  authenticate,
  checkOwnership('review'),
  reviewController.updateReview
);

// 所有者または管理者のみ削除可能
router.delete('/:id',
  authenticate,
  checkOwnership('review'),
  reviewController.deleteReview
);

module.exports = router;

出力:

TEXT 📖 参照専用
RBAC権限制御:authenticateでJWT検証、authorizeでロールチェック、checkOwnershipでリソース所有者チェック。レビューAPIはcustomer/adminが作成可能、更新・削除は所有者または管理者のみ。

❓ よくある質問

Q PUTとPATCHの違いは?
A PUTはリソース全体を置換、PATCHは部分更新。
Q ページネーションでlimitを設定すべき?
A はい、クライアントが大量データを要求するのを防ぐため、最大limit(100など)を設定。
Q バリデーションはどこで行うべき?
A 複数層で:Expressミドルウェア(リクエスト形式)、Mongooseスキーマ(データ整合性)。

📖 まとめ


📝 練習問題

  1. 基礎問題(⭐): 商品一覧APIを実装(ページネーション付き)。
  2. 基礎問題(⭐): Joiで商品作成バリデーションスキーマを定義。
  3. 応用問題(⭐⭐): 商品CRUD APIを完全に実装(5つの操作)。
  4. 応用問題(⭐⭐): RBAC権限ミドルウェアを実装(customer/adminロール)。
  5. チャレンジ(⭐⭐⭐): 完全なRESTful APIを構築(検索 + フィルタ + ページネーション + バリデーション + 権限制御)。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%