404 Not Found

404 Not Found


nginx

「総合プロジェクト:タスク管理API(第2部)— タスクのCRUDと権限管理」

1. ビジネスの焦点:アリスの2日目

翌日、アリスはCRUD操作の実装を担当し、ボブはフィルタリング、ページネーション、およびアクセス制御に注力した。「CRUDは基盤であり、フィルタリングとページネーションはユーザー体験、そしてアクセス制御はセキュリティです」とアリスは言った。「この3つすべてが不可欠です。」


2. タスクのCRUD API

(1) CRUDエンドポイントの設計

メソッド パス 説明 権限
投稿 /api/tasks タスクを作成 ログインユーザー
GET /api/tasks タスクリストを取得 ログイン中のユーザー
GET /api/tasks/:id 1つのタスクを取得 自分または管理者
PUT /api/tasks/:id タスクの更新 自分または管理者
削除 /api/tasks/:id タスクを削除 自分または管理者

▶ 例:タスクの作成

JAVASCRIPT
router.post('/', auth, async (req, res, next) => {
  try {
    const task = await Task.create({ ...req.body, assignedTo: req.user._id });
    res.status(201).json(task);
  } catch (err) {
    next(err);
  }
});

▶ 例:単一のタスクを取得する

JAVASCRIPT
router.get('/:id', auth, async (req, res, next) => {
  try {
    const task = await Task.findById(req.params.id).populate('assignedTo', 'username email');
    if (!task) return res.status(404).json({ message: 'Task not found' });
    if (task.assignedTo._id.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
      return res.status(403).json({ message: 'Forbidden' });
    }
    res.json(task);
  } catch (err) {
    next(err);
  }
});

▶ 例:タスクの更新

JAVASCRIPT
router.put('/:id', auth, async (req, res, next) => {
  try {
    const task = await Task.findById(req.params.id);
    if (!task) return res.status(404).json({ message: 'Task not found' });
    if (task.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
      return res.status(403).json({ message: 'Forbidden' });
    }
    Object.assign(task, req.body);
    await task.save();
    res.json(task);
  } catch (err) {
    next(err);
  }
});

▶ 例:タスクの削除

JAVASCRIPT
router.delete('/:id', auth, async (req, res, next) => {
  try {
    const task = await Task.findById(req.params.id);
    if (!task) return res.status(404).json({ message: 'Task not found' });
    if (task.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
      return res.status(403).json({ message: 'Forbidden' });
    }
    await task.deleteOne();
    res.json({ message: 'Task deleted' });
  } catch (err) {
    next(err);
  }
});

3. リストのフィルタリング、ページネーション、および並べ替え

(1) フィルタパラメータの説明

パラメータ 説明
status 文字列 ステータスで絞り込み ?status=completed
priority 文字列 優先度でフィルタリング ?priority=high
assignedTo ObjectId 担当者(管理者)でフィルタリング ?assignedTo=userId
dueBefore ISO日付 期限より前の日付 ?dueBefore=2025-12-31
dueAfter ISO日付 期限がこれより後の日付 ?dueAfter=2025-01-01
search 文字列 タイトルによるあいまい検索 ?search=deploy

(2) ページネーションのパラメータ

パラメータ デフォルト値 説明
page 1 現在のページ
limit 10 1ページあたりの表示件数(最大100件)
sort -createdAt 並べ替え用フィールド。接頭辞 - は降順を示す

(3) 権限ルール

役割 可視範囲 行動範囲
user 自分のタスクのみ 自分のタスクのみ
admin すべてのタスク すべてのタスク
user + クエリ assignedTo このパラメータを無視

▶ 例:フィルタリング機能付きページ分割リスト

JAVASCRIPT
router.get('/', auth, async (req, res, next) => {
  try {
    const { status, priority, dueBefore, dueAfter, search, page = 1, limit = 10, sort = '-createdAt' } = req.query;
    const filter = {};
    if (req.user.role !== 'admin') filter.assignedTo = req.user._id;
    else if (req.query.assignedTo) filter.assignedTo = req.query.assignedTo;
    if (status) filter.status = status;
    if (priority) filter.priority = priority;
    if (dueBefore || dueAfter) filter.dueDate = {};
    if (dueBefore) filter.dueDate.$lte = new Date(dueBefore);
    if (dueAfter) filter.dueDate.$gte = new Date(dueAfter);
    if (search) filter.title = { $regex: search, $options: 'i' };

    const total = await Task.countDocuments(filter);
    const tasks = await Task.find(filter)
      .populate('assignedTo', 'username email')
      .sort(sort)
      .skip((page - 1) * limit)
      .limit(Number(limit));

    res.json({ tasks, total, page: Number(page), pages: Math.ceil(total / limit) });
  } catch (err) {
    next(err);
  }
});

▶ 例:複数のフィールドによる並べ替え

JAVASCRIPT
// ?sort=-priority,createdAt  →  { priority: -1, createdAt: 1 }
const parseSort = (sortStr) => {
  const sortObj = {};
  sortStr.split(',').forEach(field => {
    if (field.startsWith('-')) sortObj[field.slice(1)] = -1;
    else sortObj[field] = 1;
  });
  return sortObj;
};

4. データ検証および認証ミドルウェア

(1) リクエスト処理のワークフロー

100%
graph LR
    A[Client Request] --> B[express-validator]
    B --> C[auth Middleware]
    C --> D[Permission Check]
    D --> E[Business Logic]
    E --> F[Standard Response]
    B -->|Verification Failed| G[400 Error]
    C -->|Not verified| H[401 Error]
    D -->|No permission| I[403 Error]

▶ 例:express-validator の検証ルール

JAVASCRIPT
const { body, query, validationResult } = require('express-validator');

const validateTask = [
  body('title').notEmpty().withMessage('Title is required').isLength({ max: 100 }).withMessage('Title too long'),
  body('status').optional().isIn(['pending', 'in-progress', 'completed']),
  body('priority').optional().isIn(['low', 'medium', 'high']),
  body('dueDate').optional().isISO8601().withMessage('Invalid date format'),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
    next();
  }
];

▶ 例:権限チェックミドルウェア

JAVASCRIPT
const requireAdmin = (req, res, next) => {
  if (req.user.role !== 'admin') return res.status(403).json({ message: 'Admin access required' });
  next();
};

const requireOwnerOrAdmin = (model) => async (req, res, next) => {
  const doc = await model.findById(req.params.id);
  if (!doc) return res.status(404).json({ message: 'Not found' });
  if (doc.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
    return res.status(403).json({ message: 'Forbidden' });
  }
  req.doc = doc;
  next();
};

▶ 例:タスクの一括削除

JAVASCRIPT
router.delete('/batch', auth, requireAdmin, async (req, res, next) => {
  try {
    const { ids } = req.body;
    const result = await Task.deleteMany({ _id: { $in: ids } });
    res.json({ deleted: result.deletedCount });
  } catch (err) {
    next(err);
  }
});

5. 包括的な例:「タスク」のルーティングを完了する

CRUD、フィルタリング、ページネーション、バリデーション、および権限管理を、単一の包括的なルーティングモジュールに統合します:

JAVASCRIPT
const router = require('express').Router();
const Task = require('../models/Task');
const auth = require('../middleware/auth');
const { body, query, validationResult } = require('express-validator');

const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
  next();
};

const checkOwner = async (req, res, next) => {
  const task = await Task.findById(req.params.id);
  if (!task) return res.status(404).json({ message: 'Task not found' });
  if (task.assignedTo.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
    return res.status(403).json({ message: 'Forbidden' });
  }
  req.task = task;
  next();
};

router.post('/', auth, [
  body('title').notEmpty().isLength({ max: 100 }),
  body('priority').optional().isIn(['low', 'medium', 'high']),
  body('dueDate').optional().isISO8601()
], validate, async (req, res, next) => {
  try {
    const task = await Task.create({ ...req.body, assignedTo: req.user._id });
    res.status(201).json(task);
  } catch (err) { next(err); }
});

router.get('/', auth, async (req, res, next) => {
  try {
    const { status, priority, search, page = 1, limit = 10, sort = '-createdAt' } = req.query;
    const filter = {};
    if (req.user.role !== 'admin') filter.assignedTo = req.user._id;
    if (status) filter.status = status;
    if (priority) filter.priority = priority;
    if (search) filter.title = { $regex: search, $options: 'i' };
    const total = await Task.countDocuments(filter);
    const tasks = await Task.find(filter).populate('assignedTo', 'username').sort(sort).skip((page - 1) * limit).limit(Number(limit));
    res.json({ tasks, total, page: Number(page), pages: Math.ceil(total / limit) });
  } catch (err) { next(err); }
});

router.get('/:id', auth, checkOwner, (req, res) => res.json(req.task));

router.put('/:id', auth, checkOwner, [
  body('title').optional().notEmpty().isLength({ max: 100 }),
  body('status').optional().isIn(['pending', 'in-progress', 'completed'])
], validate, async (req, res, next) => {
  try {
    Object.assign(req.task, req.body);
    await req.task.save();
    res.json(req.task);
  } catch (err) { next(err); }
});

router.delete('/:id', auth, checkOwner, async (req, res, next) => {
  try {
    await req.task.deleteOne();
    res.json({ message: 'Task deleted' });
  } catch (err) { next(err); }
});

module.exports = router;

❓ よくある質問

Q ページ分割クエリはどのように実装しますか?
A skiplimit を使用します。Task.countDocuments() でドキュメントの総数を取得し、Task.find().skip((page-1)*limit).limit(limit) で現在のページのデータを取得します。
Q express-validator と Joi のどちらを選べばよいですか?
A express-validator は validator.js をベースとしており、Express ミドルウェアとシームレスに統合されます。一方、Joi はより強力ですが、別途呼び出しを行う必要があります。Express プロジェクトには express-validator をお勧めします。
Q ソフト削除を実装するにはどうすればよいですか?
A スキーマに deletedAt フィールドを追加し、クエリで { deletedAt: null } を使ってフィルタリングするか、mongoose-delete プラグインを使用して自動的に処理するようにします。
Q タスクの権限はどのように管理されていますか?
A ルーティングミドルウェアにおいて、req.userIdtask.author を比較してください。作成者本人が自身のタスクを編集または削除できるのみで、それ以外のユーザーには 403 エラーが返されます。
Q 一括処理はどのように行えばよいですか?
A MongooseのbulkWriteまたはupdateManyを使用して、複数の書き込み操作を一度に実行してください。これにより、個々の操作をループで処理する場合よりもはるかに高いパフォーマンスが得られます。

📖 まとめ

📝 練習問題

  1. タスクのCRUDルートを完全に実装し、Postmanを使用して作成、照会、更新、削除の各操作を1つずつテストする。
  2. フィルタリング機能とページネーション機能を追加し、?status=completed&page=2&limit=5&sort=-priority などの組み合わせクエリをテストする。
  3. 一般ユーザーが管理画面にアクセスした際に403エラーが返されるように、requireAdmin ミドルウェアを作成してください。
  4. 登録およびログイン用の express-validator 検証ルールを追加し、空のフィールドや形式が不正な場合のエラー応答をテストする。

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%