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.”



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);
  }
});
▶ Try it Yourself

▶ 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);
  }
});
▶ Try it Yourself

▶ 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);
  }
});
▶ Try it Yourself

▶ 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);
  }
});
▶ Try it Yourself

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);
  }
});
▶ Try it Yourself

▶ 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;
};
▶ Try it Yourself

4. Data Validation and Authorization Middleware

▶ Example: (1) Request Processing Workflow

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]

▶ 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();
  }
];
▶ Try it Yourself

▶ 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();
};
▶ Try it Yourself

▶ 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);
  }
});
▶ Try it Yourself

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.

📖 Summary

📝 Exercises

  1. Implement complete Task CRUD routes and use Postman to test creation, query, update, and delete operations one by one.
  2. Add filtering and pagination features, and test combination queries such as ?status=completed&page=2&limit=5&sort=-priority.
  3. Write the requireAdmin middleware to ensure that a 403 error is returned when a regular user accesses the admin interface.
  4. Add express-validator validation rules for registration and login, and test the error responses for empty fields and invalid formats.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏