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%

16. Writing Update Controllers for User Backend

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

Update Account Details

The simplest update controller changes user profile data. We use findByIdAndUpdate with { new: true } to get the updated document back:

const updateAccountDetails = asyncHandler(async (req, res) => {
  const { fullName, email } = req.body;
  if (!fullName || !email) throw new ApiError(400, 'fullName and email are required');

  const user = await User.findByIdAndUpdate(
    req.user._id,
    { $set: { fullName, email: email.toLowerCase().trim() } },
    { new: true }   // Return the updated document
  ).select('-password -refreshToken');

  return res.status(200).json(new ApiResponse(200, user, 'Account updated'));
});

---

Change Password Controller

const changeCurrentPassword = asyncHandler(async (req, res) => {
  const { oldPassword, newPassword, confirmPassword } = req.body;

  if (newPassword !== confirmPassword) {
    throw new ApiError(400, 'New password and confirm password do not match');
  }
  if (newPassword.length < 8) throw new ApiError(400, 'Password must be at least 8 characters');

  // Fetch user WITH password (select: false means it won't be there by default)
  const user = await User.findById(req.user._id).select('+password');

  const isOldPasswordCorrect = await user.isPasswordCorrect(oldPassword);
  if (!isOldPasswordCorrect) throw new ApiError(401, 'Old password is incorrect');

  user.password = newPassword;  // Pre-save hook will hash it
  await user.save({ validateBeforeSave: false });

  return res.status(200).json(new ApiResponse(200, {}, 'Password changed successfully'));
});

---

Update Avatar (with Cloudinary cleanup)

When updating an avatar, we must:

  1. Get the new file from req.file (use upload.single('avatar') middleware)
  2. Upload new file to Cloudinary
  3. Get the old avatar URL from the current user document
  4. Extract the public_id from the old URL and delete it from Cloudinary
  5. Update the user's avatar URL in the database
const updateUserAvatar = asyncHandler(async (req, res) => {
  const avatarLocalPath = req.file?.path;
  if (!avatarLocalPath) throw new ApiError(400, 'Avatar file is required');

  const newAvatar = await uploadOnCloudinary(avatarLocalPath);
  if (!newAvatar?.secure_url) throw new ApiError(500, 'Avatar upload failed');

  // Get old avatar public_id to delete from Cloudinary
  const oldUser = await User.findById(req.user._id).select('avatar');
  if (oldUser?.avatar) {
    const publicId = oldUser.avatar.split('/').pop()?.split('.')[0];
    if (publicId) await deleteFromCloudinary(publicId);
  }

  const user = await User.findByIdAndUpdate(
    req.user._id,
    { $set: { avatar: newAvatar.secure_url } },
    { new: true }
  ).select('-password -refreshToken');

  return res.status(200).json(new ApiResponse(200, user, 'Avatar updated'));
});

---

Get User Channel Profile — Aggregation Pipeline

This is one of the most advanced controllers — it uses MongoDB aggregation to get a user's channel info including subscriber count, subscription count, and whether the current user is subscribed, all in a single query:

const getUserChannelProfile = asyncHandler(async (req, res) => {
  const { username } = req.params;
  if (!username?.trim()) throw new ApiError(400, 'Username is required');

  const channel = await User.aggregate([
    // Stage 1: Find user by username
    { $match: { username: username.toLowerCase().trim() } },

    // Stage 2: Look up subscribers (documents where channel = this user)
    {
      $lookup: {
        from: 'subscriptions',
        localField: '_id',
        foreignField: 'channel',
        as: 'subscribers',
      },
    },

    // Stage 3: Look up who this user subscribes to
    {
      $lookup: {
        from: 'subscriptions',
        localField: '_id',
        foreignField: 'subscriber',
        as: 'subscribedTo',
      },
    },

    // Stage 4: Add computed fields
    {
      $addFields: {
        subscribersCount: { $size: '$subscribers' },
        channelsSubscribedToCount: { $size: '$subscribedTo' },
        isSubscribed: {
          $cond: {
            if: { $in: [req.user?._id, '$subscribers.subscriber'] },
            then: true,
            else: false,
          },
        },
      },
    },

    // Stage 5: Project only necessary fields
    {
      $project: {
        fullName: 1,
        username: 1,
        avatar: 1,
        coverImage: 1,
        email: 1,
        subscribersCount: 1,
        channelsSubscribedToCount: 1,
        isSubscribed: 1,
        createdAt: 1,
      },
    },
  ]);

  if (!channel?.length) throw new ApiError(404, 'Channel not found');

  return res.status(200).json(new ApiResponse(200, channel[0], 'Channel profile fetched'));
});

---

Get Watch History — Nested $lookup

const getWatchHistory = asyncHandler(async (req, res) => {
  const user = await User.aggregate([
    { $match: { _id: new mongoose.Types.ObjectId(req.user._id) } },
    {
      $lookup: {
        from: 'videos',
        localField: 'watchHistory',
        foreignField: '_id',
        as: 'watchHistory',
        pipeline: [
          {
            $lookup: {
              from: 'users',
              localField: 'owner',
              foreignField: '_id',
              as: 'owner',
              pipeline: [
                { $project: { fullName: 1, username: 1, avatar: 1 } },
              ],
            },
          },
          { $addFields: { owner: { $first: '$owner' } } },
        ],
      },
    },
  ]);

  return res.status(200).json(new ApiResponse(200, user[0].watchHistory, 'Watch history fetched'));
});

---

Key Concepts Used in Update Controllers

ConceptWhat It Does
$setUpdate specific fields only
$unsetRemove a field from document
{ new: true }Return updated document from findByIdAndUpdate
{ validateBeforeSave: false }Skip schema validation when saving one field
$sizeCount array elements in aggregation
$inCheck if value exists in array
$condConditional expression (if/then/else)
$firstGet first element from array after $lookup