Node.js: Task API Project (Part 2)
Last updated: 2026-08-26
1. Business Focus: Alice's Second Day
The next day, Alice was in charge of implementing CRUD operations, while Bob focused on filtering, pagination, and access control. “CRUD is the backbone, filtering and pagination are the user experience, and access control is security,” Alice said. “All three are indispensable.”
- The CRUD API is the core business logic of task management
- Query filter allows users to filter tasks by status, priority, or date
- Pagination and sorting help prevent performance degradation when dealing with large datasets
- Role permissions ensure that regular users can only work on their own tasks
- express-validator: Standardizes the validation of request parameter formats
2. Task CRUD API
(1) CRUD Endpoint Design
| Method | Path | Description | Permissions |
|---|---|---|---|
| POST | /api/tasks |
Create Task | Logged-in User |
| GET | /api/tasks |
Get Task List | Logged-in User |
| GET | /api/tasks/:id |
Get a single task | Me or admin |
| PUT | /api/tasks/:id |
Update Task | Myself or admin |
| DELETE | /api/tasks/:id |
Delete Task | Me or admin |
▶ Example: Creating a Task
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);
}
});
▶ Example: Retrieving a Single Task
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);
}
});
▶ Example: Update Task
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);
}
});
▶ Example: Deleting a Task
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. List Filtering, Pagination, and Sorting
(1) Filter Parameter Description
| Parameter | Type | Description | Example |
|---|---|---|---|
status |
String | Filter by status | ?status=completed |
priority |
String | Filter by priority | ?priority=high |
assignedTo |
ObjectId | Filter by assignee (admin) | ?assignedTo=userId |
dueBefore |
ISO Date | Due Date Earlier Than | ?dueBefore=2025-12-31 |
dueAfter |
ISO Date | Due Date Later Than | ?dueAfter=2025-01-01 |
search |
String | Fuzzy Search by Title | ?search=deploy |
(2) Pagination Parameters
| Parameter | Default Value | Description |
|---|---|---|
page |
1 | Current Page |
limit |
10 | Items per page (max 100) |
sort |
-createdAt |
Sort field; the prefix - indicates descending order |
(3) Permission Rules
| Role | Visibility Range | Action Range |
|---|---|---|
user |
My tasks only | My tasks only |
admin |
All Tasks | All Tasks |
user + query assignedTo |
Ignore this parameter | — |
▶ Example: A paginated list with filtering
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);
}
});
▶ Example: Sorting by Multiple Fields
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. Data Validation and Authorization Middleware
▶ Example: (1) Request Processing Workflow
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]
▶ Example: express-validator validation rules
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();
}
];
▶ Example: Permission Check Middleware
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();
};
▶ Example: Deleting Tasks in Bulk
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. Comprehensive Example: Complete "tasks" Routing
Integrate CRUD, filtering, pagination, validation, and permissions into a single, comprehensive routing module:
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;
❓ FAQ
Q How do you implement paginated queries?
A Use
skip and limit: Use Task.countDocuments() to get the total number of documents, and Task.find().skip((page-1)*limit).limit(limit) to retrieve the data for the current page.Q How do I choose between express-validator and Joi?
A express-validator is based on validator.js and integrates seamlessly with Express middleware; Joi is more powerful but requires a separate call. We recommend express-validator for Express projects.
Q How do I implement soft deletion?
A Add a
deletedAt field to the schema and filter queries with { deletedAt: null }; or use the mongoose-delete plugin to handle it automatically.Q How are task permissions controlled?
A In the routing middleware, compare
req.userId and task.author. Only the author can modify or delete their own tasks; everyone else receives a 403 error.Q How do I handle bulk operations?
A Use Mongoose's
bulkWrite or updateMany to execute multiple write operations at once; this offers much better performance than looping through individual operations.- Q: How do I implement pagination? A: Use Mongoose's
.skip((page-1)*limit).limit(limit)to implement offset-based pagination, and usecountDocumentsto return the total count and calculate the total number of pages. - Q: How can I restrict users to working only on their own tasks? A: Add
assignedTo: req.user._idto the query filter, and before updating or deleting, check whethertask.assignedToequals the current user ID. - Q: How do I delete items in bulk? A: Use
Task.deleteMany({ _id: { $in: ids } }), but we recommend restricting bulk operations to users with the "admin" role. - Q: How do I handle sorting by multiple fields? A: Separate them with commas, such as
?sort=-priority,createdAt, which is parsed as{ priority: -1, createdAt: 1 }and passed to Mongoose as.sort(). - Q: How can I standardize the format of validation errors? A: Use
validationResultfrom express-validator to ensure all responses follow the{ errors: [{ msg, param, value }] }structure. - Q: Is there room for optimization in pagination performance? A: When dealing with large datasets, the
skipoperation is slow. You can switch to cursor-based pagination (using$gtfiltering based on_idorcreatedAt) to avoid skipping large numbers of documents.
📖 Summary
- Business Focus: Key Concepts and Usage of Alice on Day Two
- Core Concepts and Usage of the CRUD API
- Core Concepts and Usage of List Filtering, Pagination, and Sorting
- Core Concepts and Usage of Data Validation and Authorization Middleware
- Comprehensive Example: Core Concepts and Usage of the Complete "tasks" Route
📝 Exercises
- Implement complete Task CRUD routes and use Postman to test creation, query, update, and delete operations one by one.
- Add filtering and pagination features, and test combination queries such as
?status=completed&page=2&limit=5&sort=-priority. - Write the
requireAdminmiddleware to ensure that a 403 error is returned when a regular user accesses the admin interface. - Add express-validator validation rules for registration and login, and test the error responses for empty fields and invalid formats.