Mongoose Update Methods Compared: save(), updateOne(), findOneAndUpdate(), and replaceOne()
MongooseMongoDBCRUDAPI DevelopmentCode Patterns

Mongoose Update Methods Compared: save(), updateOne(), findOneAndUpdate(), and replaceOne()

MMongoose Cloud Editorial Team
2026-11-09
6 min read

Compare Mongoose save(), updateOne(), findOneAndUpdate(), and replaceOne() for validation, middleware, replacements, and returned documents.

Choosing the right Mongoose update method affects validation, middleware, timestamps, returned data, and the risk of overwriting fields. This guide compares save(), updateOne(), findOneAndUpdate(), and replaceOne() so you can select a safe pattern for APIs, jobs, and maintenance scripts.

Overview

Mongoose offers several ways to write changes to MongoDB, but they are not interchangeable. The most important distinction is whether you are modifying a loaded document, applying an update operator to a matching document, or replacing the stored document with a new representation.

MethodTypical purposeReturnsMain consideration
save()Edit a document you have loadedThe saved documentUsually provides the clearest document validation and save middleware behavior
updateOne()Apply a targeted update without loading the documentA write resultEfficient, but does not return the updated document
findOneAndUpdate()Find one document and update it in one operationThe matched document, before or after the updateConfigure return behavior and update validation explicitly
replaceOne()Replace an entire matching documentA write resultOmitted fields can disappear, so it requires a complete replacement object

As a practical default, use save() when you already have a document and want document-oriented behavior. Use updateOne() for a focused write where the write result is enough. Choose findOneAndUpdate() when you need the changed document returned atomically. Reserve replaceOne() for deliberate full-document replacement.

How to compare options

Before selecting a method, answer five questions:

  1. Do you need the current document? If the operation depends on values already stored, loading the document and calling save() makes the decision logic explicit. If you need one atomic find-and-update operation, consider findOneAndUpdate().
  2. Is the change partial or complete? Use operators such as $set, $inc, and $unset for partial edits. Use replaceOne() only when the replacement represents the complete intended document.
  3. What should the caller receive? updateOne() and replaceOne() return write metadata, such as matched and modified counts. findOneAndUpdate() can return the document before or after the update. save() returns the saved document instance.
  4. Which validation and middleware must run? Document saves and query-based updates do not have identical middleware paths. Treat runValidators and middleware expectations as part of the method choice, not as afterthoughts.
  5. Could two writers change the same record? A read-edit-save sequence can require optimistic concurrency controls or a transaction when competing writes matter. For multi-document consistency, review the Mongoose transactions tutorial.

Also separate authorization from persistence. A request should first establish which document the caller may edit, then construct an allowlisted update. Do not pass an entire request body directly to a Mongoose update method when clients could modify protected fields.

Feature-by-feature breakdown

save(): document-first updates

The common pattern is:

const user = await User.findById(userId);
if (!user) throw new Error('User not found');

user.displayName = input.displayName;
user.preferences.theme = input.theme;
await user.save();

This approach is readable when business rules depend on the existing document. Mongoose document validation runs as part of saving, and document middleware such as pre('save') and post('save') is designed for this path. It also gives you the saved document for a response or a subsequent operation.

The tradeoff is an extra read and a possible lost-update problem if another writer changes the document between the read and save. For concurrent workflows, consider versioning, an atomic predicate, or a transaction. See the transaction error-handling guide for a production workflow.

updateOne(): targeted writes without a returned document

const result = await User.updateOne(
  { _id: userId },
  { $set: { 'preferences.theme': input.theme } },
  { runValidators: true }
);

This is a good fit for small, direct changes, bulk-style jobs, and endpoints that only need to report success. It avoids loading the document, but result is not the updated user. Check the result and handle the case where no document matched.

Update validators are not identical to full document validation. They generally apply to the paths being updated, and some validators or required-field assumptions behave differently when only a subset of fields is present. Use runValidators: true when appropriate, and test the actual update shapes your application sends.

findOneAndUpdate(): atomic match, update, and retrieval

const updatedUser = await User.findOneAndUpdate(
  { _id: userId, status: 'active' },
  { $set: { displayName: input.displayName } },
  {
    runValidators: true,
    returnDocument: 'after'
  }
);

The filter can enforce a condition, and the operation can return the changed document. Depending on the Mongoose and MongoDB driver versions in your project, you may also see new: true used for the after-update document. Use the option style supported by your installed versions and keep this behavior covered by tests.

By default, find-and-update methods historically return the document from before the update unless configured otherwise. Query middleware applies to this method; it does not automatically provide the same behavior as document save() middleware. If an audit plugin or business rule depends on a particular hook, verify its implementation.

replaceOne(): intentional full replacement

const result = await User.replaceOne(
  { _id: userId },
  {
    _id: userId,
    email: normalizedEmail,
    displayName: input.displayName,
    preferences: { theme: input.theme }
  },
  { runValidators: true }
);

Unlike $set, replacement semantics mean fields omitted from the replacement may be removed. That makes this method useful for importing a canonical representation or synchronizing a complete record, but dangerous for ordinary PATCH endpoints. Treat the replacement object as a full contract, preserve the identity field correctly, and confirm how schema timestamps, defaults, immutable paths, and strict mode behave in your installed Mongoose version.

Schema timestamps can be applied differently across write methods and configurations. If created or updated times are operationally important, verify them with integration tests rather than assuming every method has identical behavior.

Best fit by scenario

  • PATCH endpoint with a few editable fields: use updateOne() or findOneAndUpdate() with an allowlisted $set object and runValidators: true.
  • Endpoint that must return the changed resource: use findOneAndUpdate() with an explicit after-update return option, or load and save when business logic requires the document first.
  • Complex business rule based on current values: load the document, validate authorization and state transitions, then call save(). Add concurrency protection when needed.
  • Background counter or status update: use an atomic operator with updateOne() or findOneAndUpdate(), and make the filter reflect the allowed state transition.
  • Import or full synchronization: use replaceOne() only after constructing and validating a complete representation.
  • Audit requirements: confirm whether your auditing code listens to save middleware, query middleware, or both. A reusable plugin can help standardize this; see the Mongoose plugins guide.

A safe API workflow is: authenticate the caller, load or constrain the target, allowlist fields, choose operators, enable the needed validation, check the result, and test the response shape. For tests, cover missing matches, invalid values, protected fields, timestamps, middleware side effects, and concurrent updates. The Mongoose testing guide provides a useful structure for unit and integration coverage.

When to revisit

Revisit this comparison when you upgrade Mongoose, the MongoDB Node.js driver, or MongoDB itself; update schema timestamps or plugins; introduce optimistic concurrency; or change an endpoint from full replacement to partial editing. Method signatures, option names, validation details, middleware behavior, and compatibility notes can change across supported versions.

As a practical maintenance checklist, record the versions used by the application, add an integration test for every update style, and document whether each endpoint expects a write result or a returned document. During code review, ask three questions: could this update remove fields, are validators actually enabled for the intended paths, and does the selected method run the middleware the application relies on? Those checks make the choice durable even as models and dependencies evolve.

Related Topics

#Mongoose#MongoDB#CRUD#API Development#Code Patterns
M

Mongoose Cloud Editorial Team

Developer Tools Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.