Siksha Sarovar

Siksha Sarovar (sikshasarovar.com) is a free educational web application that helps students in India learn programming and prepare for academic and competitive exams. The platform offers structured coding courses (C, C++, Python, Java, HTML, CSS, PHP, Power BI, AI, Machine Learning, Data Science), complete university curriculum notes for BCA/MCA students with previous year question papers, Class 10 and Class 12 CBSE/HBSE school notes, and dedicated preparation material for SSC, UPSC, Banking, Railway and other government exams. Browsing the site is completely free and requires no account. Users may optionally sign in with Google solely to save their learning progress, quiz scores and personal preferences across devices.

Privacy Policy | Terms of Service | Contact Siksha Sarovar | About Siksha Sarovar

v4.0.9 · PWA
Siksha Sarovar logo
Siksha Sarovar
Your Learning Universe

Siksha Sarovar is a free e-learning platform for coding courses, BCA university notes and competitive exam preparation. Optional Google sign-in saves your learning progress across devices.

Initializing knowledge base…
Compiling modules 0%

21. Essential Controllers — Comments, Like, Playlist, Tweets, Videos

Lesson 21 of 23 in the free Backend Development notes on Siksha Sarovar, written by Rohit Jangra.

Overview

This lesson covers the essential controller implementations for all the remaining features of our YouTube-like backend. Each controller follows the same pattern: validate inputs → check authorization → perform DB operation → return ApiResponse.

---

Comment Controllers

addComment:

const addComment = asyncHandler(async (req, res) => {
  const { videoId } = req.params;
  const { content } = req.body;
  if (!content?.trim()) throw new ApiError(400, 'Comment content is required');
  if (!mongoose.isValidObjectId(videoId)) throw new ApiError(400, 'Invalid video ID');

  const video = await Video.findById(videoId);
  if (!video) throw new ApiError(404, 'Video not found');

  const comment = await Comment.create({ content: content.trim(), video: videoId, owner: req.user._id });
  return res.status(201).json(new ApiResponse(201, comment, 'Comment added'));
});

updateComment and deleteComment — always check ownership before allowing update/delete:

const deleteComment = asyncHandler(async (req, res) => {
  const comment = await Comment.findById(req.params.commentId);
  if (!comment) throw new ApiError(404, 'Comment not found');
  if (comment.owner.toString() !== req.user._id.toString()) {
    throw new ApiError(403, 'Unauthorized to delete this comment');
  }
  await Comment.findByIdAndDelete(req.params.commentId);
  // Also delete all likes on this comment
  await Like.deleteMany({ comment: req.params.commentId });
  return res.status(200).json(new ApiResponse(200, {}, 'Comment deleted'));
});

getVideoComments with pagination and like info:

const getVideoComments = asyncHandler(async (req, res) => {
  const { videoId } = req.params;
  const { page = 1, limit = 10 } = req.query;
  const skip = (Number(page) - 1) * Number(limit);

  const comments = await Comment.aggregate([
    { $match: { video: new mongoose.Types.ObjectId(videoId) } },
    { $sort: { createdAt: -1 } },
    { $skip: skip },
    { $limit: Number(limit) },
    {
      $lookup: { from: 'users', localField: 'owner', foreignField: '_id', as: 'owner',
        pipeline: [{ $project: { fullName: 1, username: 1, avatar: 1 } }],
      },
    },
    {
      $lookup: { from: 'likes', localField: '_id', foreignField: 'comment', as: 'likes' },
    },
    {
      $addFields: {
        owner: { $first: '$owner' },
        likesCount: { $size: '$likes' },
        isLiked: { $in: [new mongoose.Types.ObjectId(req.user._id), '$likes.likedBy'] },
      },
    },
    { $project: { content: 1, owner: 1, likesCount: 1, isLiked: 1, createdAt: 1 } },
  ]);

  const total = await Comment.countDocuments({ video: videoId });
  return res.status(200).json(new ApiResponse(200, { comments, total, page, limit }, 'Comments fetched'));
});

---

Like Controllers

toggleVideoLike:

const toggleVideoLike = asyncHandler(async (req, res) => {
  const { videoId } = req.params;
  const existing = await Like.findOne({ likedBy: req.user._id, video: videoId });
  if (existing) {
    await Like.findByIdAndDelete(existing._id);
    return res.status(200).json(new ApiResponse(200, { isLiked: false }, 'Like removed'));
  }
  await Like.create({ likedBy: req.user._id, video: videoId });
  return res.status(200).json(new ApiResponse(200, { isLiked: true }, 'Video liked'));
});

getLikedVideos:

const getLikedVideos = asyncHandler(async (req, res) => {
  const likes = await Like.find({ likedBy: req.user._id, video: { $exists: true } })
    .populate({ path: 'video', select: 'title thumbnail duration views owner', populate: { path: 'owner', select: 'username avatar' } })
    .sort({ createdAt: -1 });
  const videos = likes.map((l) => l.video).filter(Boolean);
  return res.status(200).json(new ApiResponse(200, videos, 'Liked videos fetched'));
});

---

Playlist Controllers

const addVideoToPlaylist = asyncHandler(async (req, res) => {
  const { playlistId, videoId } = req.params;
  const playlist = await Playlist.findById(playlistId);
  if (!playlist) throw new ApiError(404, 'Playlist not found');
  if (playlist.owner.toString() !== req.user._id.toString())
    throw new ApiError(403, 'Unauthorized');

  const video = await Video.findById(videoId);
  if (!video) throw new ApiError(404, 'Video not found');

  const updated = await Playlist.findByIdAndUpdate(
    playlistId,
    { $addToSet: { videos: videoId } },  // $addToSet prevents duplicates
    { new: true }
  );
  return res.status(200).json(new ApiResponse(200, updated, 'Video added to playlist'));
});

---

Video Controllers — Pagination Pattern

getAllVideos with search, sort, and pagination:

const getAllVideos = asyncHandler(async (req, res) => {
  const { page = 1, limit = 10, query = '', sortBy = 'createdAt', sortType = 'desc', userId } = req.query;
  const skip = (Number(page) - 1) * Number(limit);

  const matchStage = { isPublished: true };
  if (query) matchStage['$text'] = { $search: query };
  if (userId && mongoose.isValidObjectId(userId)) matchStage['owner'] = new mongoose.Types.ObjectId(userId);

  const videos = await Video.aggregate([
    { $match: matchStage },
    { $lookup: { from: 'users', localField: 'owner', foreignField: '_id', as: 'owner',
        pipeline: [{ $project: { fullName: 1, username: 1, avatar: 1 } }] } },
    { $addFields: { owner: { $first: '$owner' } } },
    { $sort: { [sortBy]: sortType === 'asc' ? 1 : -1 } },
    { $skip: skip },
    { $limit: Number(limit) },
    { $project: { title: 1, thumbnail: 1, duration: 1, views: 1, owner: 1, createdAt: 1 } },
  ]);

  const total = await Video.countDocuments(matchStage);
  return res.status(200).json(new ApiResponse(200, { videos, total, page, limit }, 'Videos fetched'));
});

---

Key Controller Patterns Summary

PatternImplementation
Ownership checkdoc.owner.toString() !== req.user._id.toString()
Paginationskip = (page-1) * limit, $skip + $limit in aggregation
TogglefindOne → delete if exists, create if not
Prevent duplicates$addToSet in $set update
Remove from array$pull: { array: value }
Cascade deleteDelete related documents (likes on deleted comment)
Validate ObjectIdmongoose.isValidObjectId(id) before DB queries