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:
- Get the new file from
req.file(useupload.single('avatar')middleware) - Upload new file to Cloudinary
- Get the old avatar URL from the current user document
- Extract the
public_idfrom the old URL and delete it from Cloudinary - 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
| Concept | What It Does |
|---|---|
$set | Update specific fields only |
$unset | Remove a field from document |
{ new: true } | Return updated document from findByIdAndUpdate |
{ validateBeforeSave: false } | Skip schema validation when saving one field |
$size | Count array elements in aggregation |
$in | Check if value exists in array |
$cond | Conditional expression (if/then/else) |
$first | Get first element from array after $lookup |