「総合プロジェクト:タスク管理API(第2部)— タスクのCRUDと権限管理」
1. ビジネスの焦点:アリスの2日目
翌日、アリスはCRUD操作の実装を担当し、ボブはフィルタリング、ページネーション、およびアクセス制御に注力した。「CRUDは基盤であり、フィルタリングとページネーションはユーザー体験、そしてアクセス制御はセキュリティです」とアリスは言った。「この3つすべてが不可欠です。」
- CRUD APIは、タスク管理の中核となるビジネスロジックです
- クエリフィルターを使用すると、ユーザーはタスクをステータス、優先度、または日付で絞り込むことができます
- ページネーションとソートは、大規模なデータセットを扱う際のパフォーマンス低下を防ぐのに役立ちます
- 役割ごとの権限設定により、一般ユーザーは自分のタスクのみを処理できるようになります
- express-validator: リクエストパラメータの形式検証を標準化します
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) リクエスト処理のワークフロー
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
skip と limit を使用します。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.userId と task.author を比較してください。作成者本人が自身のタスクを編集または削除できるのみで、それ以外のユーザーには 403 エラーが返されます。Q 一括処理はどのように行えばよいですか?
A Mongooseの
bulkWriteまたはupdateManyを使用して、複数の書き込み操作を一度に実行してください。これにより、個々の操作をループで処理する場合よりもはるかに高いパフォーマンスが得られます。- Q: ページネーションを実装するにはどうすればよいですか? A: Mongooseの
.skip((page-1)*limit).limit(limit)を使用してオフセットベースのページネーションを実装し、countDocumentsを使用して総件数を取得し、ページ総数を計算します。 - Q: ユーザーが自分のタスクのみを処理できるように制限するにはどうすればよいですか? A: クエリフィルターに
assignedTo: req.user._idを追加し、更新または削除を行う前に、task.assignedToが現在のユーザーIDと一致するかどうかを確認してください。 - Q: アイテムを一括で削除するにはどうすればよいですか? A:
Task.deleteMany({ _id: { $in: ids } })を使用してください。ただし、一括操作は「admin」ロールのユーザーに限定することをお勧めします。 - Q: 複数のフィールドによるソートはどのように処理すればよいですか? A: カンマで区切って指定します。例えば、
?sort=-priority,createdAtと指定すると、これは{ priority: -1, createdAt: 1 }として解析され、Mongoose には.sort()として渡されます。 - Q: バリデーションエラーの形式を統一するにはどうすればよいですか? A: express-validator の
validationResultを使用し、すべてのレスポンスが{ errors: [{ msg, param, value }] }の構造に従うようにします。 - Q: ページネーションのパフォーマンスには改善の余地がありますか? A: 大規模なデータセットを扱う場合、
skip操作の処理に時間がかかります。多数のドキュメントをスキップしないようにするには、カーソルベースのページネーション(_idまたはcreatedAtに基づく$gtフィルタリングを使用)に切り替えることができます。
📖 まとめ
- ビジネスの焦点:2日目の「Alice」の主要概念と活用法
- CRUD APIの基本概念と使い方
- リストのフィルタリング、ページネーション、ソートの基本概念と使い方
- データ検証および認証ミドルウェアの基本概念と活用方法
- 包括的な例:完全な「tasks」ルートの基本概念と使用方法
📝 練習問題
- タスクのCRUDルートを完全に実装し、Postmanを使用して作成、照会、更新、削除の各操作を1つずつテストする。
- フィルタリング機能とページネーション機能を追加し、
?status=completed&page=2&limit=5&sort=-priorityなどの組み合わせクエリをテストする。 - 一般ユーザーが管理画面にアクセスした際に403エラーが返されるように、
requireAdminミドルウェアを作成してください。 - 登録およびログイン用の express-validator 検証ルールを追加し、空のフィールドや形式が不正な場合のエラー応答をテストする。



