Updating Documents — updateOne, updateMany and Update Operators

Updating documents is one of the most nuanced operations in MongoDB. Unlike SQL’s UPDATE SET, MongoDB has a rich set of update operators that let you modify specific fields, increment counters, push items onto arrays, and pull items out — all atomically and without fetching the document first. Getting updates wrong leads to data loss (accidentally replacing documents), stale data (not running validators), or race conditions (non-atomic read-modify-write). This lesson covers every update pattern you will use in the MERN Blog, from simple field changes to atomic array modifications.

Update Methods Overview

Method What It Does Returns
Model.updateOne(filter, update) Update first matching document { matchedCount, modifiedCount }
Model.updateMany(filter, update) Update all matching documents { matchedCount, modifiedCount }
Model.findByIdAndUpdate(id, update, opts) Update by ID, return document Document (before or after update)
Model.findOneAndUpdate(filter, update, opts) Update first match, return document Document (before or after update)
doc.save() Save changes made to a fetched document The updated document
Note: updateOne() and updateMany() return a result object with matchedCount and modifiedCount — they do not return the updated document. When you need the updated document (to send back to React), use findByIdAndUpdate(id, update, { new: true }). The new: true option makes Mongoose return the document after the update instead of before.
Tip: When you need to update a document and also run some logic based on the previous value (e.g. only increment a counter if the current value is below a threshold), fetch the document first with findById(), modify it in JavaScript, then call doc.save(). This approach is clearer than complex update expressions and runs all Mongoose validators and hooks.
Warning: Always use update operators like $set, $inc, $push in your update argument. If you pass a plain object without operators — Post.updateOne({ _id: id }, { title: 'New' }) — MongoDB replaces the entire document with { title: 'New' }, destroying all other fields. This is one of the most destructive and common MongoDB mistakes.

The $set Operator — Update Specific Fields

// ── updateOne with $set ────────────────────────────────────────────────────────
await Post.updateOne(
  { _id: postId },                      // filter — which document to update
  { $set: { published: true,            // update — what to change (only these fields)
             publishedAt: new Date() } }
);

// ── findByIdAndUpdate with $set — returns the updated document ─────────────────
const updated = await Post.findByIdAndUpdate(
  postId,
  { $set: { title: 'Updated Title', body: 'Updated content' } },
  { new: true, runValidators: true }    // new: true = return updated doc
);

// ── Update only the fields provided in req.body ────────────────────────────────
// Build a $set object from only the fields that were sent
const { title, body, excerpt, tags, coverImage } = req.body;
const updates = {};
if (title      !== undefined) updates.title      = title;
if (body       !== undefined) updates.body       = body;
if (excerpt    !== undefined) updates.excerpt    = excerpt;
if (tags       !== undefined) updates.tags       = tags;
if (coverImage !== undefined) updates.coverImage = coverImage;

const post = await Post.findByIdAndUpdate(
  postId,
  { $set: updates },
  { new: true, runValidators: true }
);

Common Update Operators

// ── $set — set specific fields ─────────────────────────────────────────────────
Post.updateOne({ _id: id }, { $set: { published: true } });

// ── $unset — remove a field entirely ──────────────────────────────────────────
Post.updateOne({ _id: id }, { $unset: { excerpt: "" } });
// The value in $unset does not matter — the field is removed regardless

// ── $inc — increment or decrement a number ────────────────────────────────────
Post.updateOne({ _id: id }, { $inc: { viewCount: 1 } });   // +1
Post.updateOne({ _id: id }, { $inc: { viewCount: -1 } });  // -1

// ── $push — add an element to an array ────────────────────────────────────────
Post.updateOne({ _id: id }, { $push: { tags: 'mongodb' } });

// ── $addToSet — add to array only if not already present (no duplicates) ──────
Post.updateOne({ _id: id }, { $addToSet: { likedBy: userId } });
// If userId is already in likedBy — no change. Otherwise appended.

// ── $pull — remove matching elements from an array ────────────────────────────
Post.updateOne({ _id: id }, { $pull: { tags: 'old-tag' } });
Post.updateOne({ _id: id }, { $pull: { likedBy: userId } }); // unlike a post

// ── $rename — rename a field ──────────────────────────────────────────────────
Post.updateMany({}, { $rename: { 'content': 'body' } }); // rename all at once

// ── Combining multiple operators ──────────────────────────────────────────────
Post.updateOne({ _id: id }, {
  $set:  { published: true, publishedAt: new Date() },
  $inc:  { viewCount: 1 },
  $push: { tags: 'featured' },
});

Upsert — Update or Create

// upsert: true — update if found, insert if not found
// Useful for "set if exists, create if not" patterns

await UserSettings.findOneAndUpdate(
  { user: userId },                         // filter
  { $set: { theme: 'dark', language: 'en' } }, // update
  { upsert: true, new: true }               // create if missing
);
// If settings document exists → updates theme and language
// If not → creates { user: userId, theme: 'dark', language: 'en' }

Fetch-Modify-Save Pattern

// When you need conditional update logic or to run all hooks:
const post = await Post.findById(postId);
if (!post) throw new AppError('Post not found', 404);
if (post.author.toString() !== req.user.id) throw new AppError('Not authorised', 403);

// Modify the document in JavaScript
post.title     = req.body.title   || post.title;
post.body      = req.body.body    || post.body;
post.published = req.body.published !== undefined ? req.body.published : post.published;

if (!post.published && req.body.published === true) {
  post.publishedAt = new Date(); // set publishedAt only on first publish
}

// Saves the modified document — runs all validators and pre/post save hooks
const saved = await post.save();
res.json({ success: true, data: saved });

Common Mistakes

Mistake 1 — Replacement update instead of $set

❌ Wrong — passing a plain object replaces the entire document:

await Post.updateOne({ _id: id }, { title: 'New Title' });
// The document is now ONLY: { _id: id, title: 'New Title' }
// All other fields (body, author, tags, published...) are GONE

✅ Correct — always use an update operator:

await Post.updateOne({ _id: id }, { $set: { title: 'New Title' } }); // ✓

Mistake 2 — Not passing { new: true } to findByIdAndUpdate

❌ Wrong — the returned document is the state BEFORE the update:

const post = await Post.findByIdAndUpdate(id, { $set: { published: true } });
console.log(post.published); // false — pre-update state returned by default

✅ Correct — pass { new: true } to get the updated document:

const post = await Post.findByIdAndUpdate(
  id, { $set: { published: true } }, { new: true }
);
console.log(post.published); // true ✓

Mistake 3 — Not using $addToSet for arrays that should have unique values

❌ Wrong — using $push allows duplicates in an array that should be a set:

// User likes the same post twice
await Post.updateOne({ _id: id }, { $push: { likedBy: userId } });
await Post.updateOne({ _id: id }, { $push: { likedBy: userId } });
// likedBy: [userId, userId] — duplicate! Like count is wrong

✅ Correct — use $addToSet for unique membership:

await Post.updateOne({ _id: id }, { $addToSet: { likedBy: userId } }); // idempotent ✓

Quick Reference

Task Code
Set specific fields { $set: { field: value } }
Remove a field { $unset: { field: "" } }
Increment number { $inc: { count: 1 } }
Add to array { $push: { arr: value } }
Add unique to array { $addToSet: { arr: value } }
Remove from array { $pull: { arr: value } }
Get updated document findByIdAndUpdate(id, upd, { new: true })
Run validators on update { new: true, runValidators: true }
Upsert findOneAndUpdate(filter, upd, { upsert: true })

🧠 Test Yourself

A user clicks “Like” on a blog post. Your API calls Post.updateOne({ _id: postId }, { $addToSet: { likedBy: userId } }). The user then clicks “Like” again. What happens?