Mid-level 5 min · March 05, 2026

MongoDB CRUD — The updateOne Without $Set Data Wipe

Production failure: a plain document in updateOne wiped 10,000 order fields.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • MongoDB CRUD uses documents in collections: InsertOne/InsertMany (create), find/findOne (read), updateOne/updateMany with operators (update), deleteOne/deleteMany (delete).
  • The _id field is auto-generated as an ObjectId — capture it immediately after insert to avoid extra queries.
  • Update operators ($set, $inc, $push) prevent accidental document replacement; never pass a plain object as the update argument.
  • find() returns a cursor, not an array — always chain .toArray() or iterate; use .limit() on open-ended queries to protect production.
  • Soft-delete (isDeleted flag + deletedAt timestamp) is the production standard; physical deletion reserved for cleanup or GDPR erasure.
Plain-English First

Imagine MongoDB is a giant filing cabinet where each drawer is a 'collection' and each folder inside is a 'document'. CRUD is just the four things you'd ever do to that cabinet: drop in a new folder (Create), read what's inside one (Read), scribble changes on it (Update), or shred it (Delete). That's it — every database operation in existence boils down to these four actions.

Every application that stores data — whether it's a social media feed, an e-commerce cart, or a hospital records system — needs to talk to a database. MongoDB has become the go-to choice for teams building flexible, fast-moving products because it stores data as JSON-like documents instead of rigid rows and columns. Knowing how to speak its language isn't optional; it's the difference between an app that ships and one that stalls.

Create — Inserting Documents the Right Way

Inserting data sounds trivial until you're doing it wrong in production. MongoDB gives you two insertion methods: insertOne() for a single document and insertMany() for a batch. The key insight most tutorials skip is what MongoDB hands back after an insert — an acknowledgement object containing the auto-generated _id. That _id is a 12-byte ObjectId, globally unique by design, and you should be capturing it in your application logic rather than ignoring it.

Why does this matter? Because in a typical e-commerce flow you insert an order document, then immediately need that order's _id to create a shipment record that references it. Ignoring the return value forces a second round-trip to the database just to find what MongoDB already told you.

insertMany() is more nuanced. By default it's ordered, meaning if document number 3 of 10 fails validation, documents 1 and 2 are already committed and 4-10 are abandoned. Passing the option { ordered: false } lets MongoDB push through all valid documents and collect errors at the end — a much better pattern for bulk imports where one bad record shouldn't kill the whole batch.

insertOrders.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Connect to a local MongoDB instance using the official Node.js driver
const { MongoClient, ObjectId } = require('mongodb');

async function insertOrders() {
  const client = new MongoClient('mongodb://localhost:27017');

  try {
    await client.connect();
    const db = client.db('storefront');           // target database
    const ordersCollection = db.collection('orders'); // target collection

    // --- insertOne: single document ---
    // MongoDB auto-generates _id if you don't provide one
    const singleInsertResult = await ordersCollection.insertOne({
      customerId: 'cust_8821',
      items: [
        { productSku: 'SHOE-RED-42', quantity: 1, unitPrice: 79.99 },
        { productSku: 'LACE-BLK',   quantity: 2, unitPrice:  3.50 }
      ],
      orderStatus: 'pending',
      createdAt: new Date()                        // always store dates as Date objects, not strings
    });

    // insertedId is the ObjectId MongoDB assigned — save this, don't query for it again
    console.log('New order ID:', singleInsertResult.insertedId);

    // --- insertMany: bulk insert with ordered:false so one bad doc doesn't abort the rest ---
    const bulkOrders = [
      {
        customerId: 'cust_1103',
        items: [{ productSku: 'HAT-GRN-M', quantity: 1, unitPrice: 24.00 }],
        orderStatus: 'pending',
        createdAt: new Date()
      },
      {
        customerId: 'cust_4477',
        items: [{ productSku: 'BELT-BRN-L', quantity: 1, unitPrice: 34.50 }],
        orderStatus: 'pending',
        createdAt: new Date()
      }
    ];

    const bulkInsertResult = await ordersCollection.insertMany(bulkOrders, {
      ordered: false   // continue inserting even if one document fails validation
    });

    // insertedCount tells you how many actually landed
    console.log('Orders inserted:', bulkInsertResult.insertedCount);
    console.log('Inserted IDs:', bulkInsertResult.insertedIds);

  } finally {
    await client.close();                          // always close the connection
  }
}

insertOrders().catch(console.error);
Output
New order ID: 64f3a2b1c9e77f001a3d8e12
Orders inserted: 2
Inserted IDs: { '0': 64f3a2b1c9e77f001a3d8e13, '1': 64f3a2b1c9e77f001a3d8e14 }
Pro Tip: Capture insertedId Immediately
After insertOne(), grab result.insertedId right there in the same function. Every extra database round-trip to 'find' the document you just created is wasted latency you already paid for.
Production Insight
Inserting into a shard cluster with a monotonically increasing _id (like a timestamp) can cause hotspotting on one shard. Use a random or hash-based shard key.
Always handle duplicate key errors (E11000) in your code — they're not bugs, they're design invariants.
Rule: choose _id generation strategy based on your write pattern.
Key Takeaway
Capture insertedId immediately after insertOne().
Use {ordered:false} for bulk inserts when one failure shouldn't abort the batch.
Never ignore the result object from insert operations.

Read — Querying Documents Without Killing Your Database

Reading data is where most MongoDB performance problems are born. find() returns a cursor, not an array — meaning MongoDB streams results lazily rather than loading everything into memory at once. This is a feature, not a quirk, and it matters the moment your collection grows past a few thousand documents.

The filter argument is where the real power lives. MongoDB's query language is composable: you can filter by exact match, range ($gte, $lte), array membership ($in), logical operators ($and, $or), and even run regex searches — all within a single query object. But power without discipline is dangerous. Running find({}) on a million-document collection with no limit() is how you bring a production server to its knees.

Projection is the query-level equivalent of SELECT in SQL — it tells MongoDB which fields to return. Always use it. Fetching a 40-field customer document when your UI only needs name and email wastes bandwidth, serialization time, and memory on both sides of the wire.

Indexes are what make reads fast, but that's a separate topic. The habit to build now is: every field you filter or sort on should eventually have an index behind it. Use explain('executionStats') on any query you care about to see whether MongoDB is doing a full collection scan (bad) or an index scan (good).

queryOrders.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const { MongoClient } = require('mongodb');

async function queryOrders() {
  const client = new MongoClient('mongodb://localhost:27017');

  try {
    await client.connect();
    const ordersCollection = client.db('storefront').collection('orders');

    // --- findOne: grab the first match, returns a plain object (not a cursor) ---
    const latestPendingOrder = await ordersCollection.findOne(
      { orderStatus: 'pending' },                 // filter: only pending orders
      {
        projection: { customerId: 1, items: 1, createdAt: 1, _id: 0 },  // only return these fields
        sort: { createdAt: -1 }                   // newest first
      }
    );
    console.log('Latest pending order:', latestPendingOrder);

    // --- find with range filter: orders created in the last 7 days ---
    const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);

    const recentOrders = await ordersCollection
      .find(
        {
          createdAt: { $gte: sevenDaysAgo },       // $gte = greater than or equal to
          orderStatus: { $in: ['pending', 'processing'] }  // $in matches any value in the array
        },
        {
          projection: { customerId: 1, orderStatus: 1, createdAt: 1 }
        }
      )
      .sort({ createdAt: -1 })
      .limit(25)                                  // ALWAYS limit open-ended queries in production
      .toArray();                                 // materialise the cursor into an array

    console.log(`Found ${recentOrders.length} recent orders`);
    recentOrders.forEach(order => {
      console.log(`  ${order.customerId} — ${order.orderStatus} — ${order.createdAt.toISOString()}`);
    });

    // --- countDocuments: how many total pending orders exist? ---
    // Use countDocuments() NOT count() — count() is deprecated and ignores filters in some edge cases
    const pendingTotal = await ordersCollection.countDocuments({ orderStatus: 'pending' });
    console.log('Total pending orders:', pendingTotal);

  } finally {
    await client.close();
  }
}

queryOrders().catch(console.error);
Output
Latest pending order: { customerId: 'cust_4477', items: [ { productSku: 'BELT-BRN-L', quantity: 1, unitPrice: 34.5 } ], createdAt: 2024-09-02T14:23:01.000Z }
Found 3 recent orders
cust_4477 — pending — 2024-09-02T14:23:01.000Z
cust_1103 — pending — 2024-09-02T14:22:58.000Z
cust_8821 — pending — 2024-09-02T14:22:55.000Z
Total pending orders: 3
Watch Out: find() Is Not an Array
find() returns a Cursor object. Logging it directly prints cursor metadata, not your documents. Always chain .toArray() or iterate with for await...of. Forgetting this is a rite of passage — and a runtime bug that's surprisingly hard to spot.
Production Insight
A find() with no .limit() can kill your application and database under load. Always paginate or limit.
Missing indexes on filter fields lead to full collection scans; use explain() to verify.
Rule: every query filter and sort field that runs more than once a day needs an index.
Key Takeaway
find() returns a lazy Cursor — materialise with .toArray() or iterate.
Always use projection to fetch only needed fields.
Add .limit() to every open-ended query before it hits production traffic.

Update — Changing Data Without Replacing It

Updating is where MongoDB beginners most often shoot themselves. The critical rule: always use an update operator like $set, $inc, or $push. If you pass a plain document as the second argument to updateOne(), MongoDB treats it as a full replacement and wipes every field not in your update object. That's a legal operation, but it's almost never what you want.

MongoDB's update operators are surgical. $set modifies only the fields you name, leaving everything else untouched. $inc atomically increments a number — perfect for view counters or inventory tracking without a read-modify-write cycle. $push appends to an array, and $pull removes from one. $unset deletes a field entirely.

The upsert option is powerful but underused. Setting { upsert: true } tells MongoDB: if the filter matches something, update it; if nothing matches, create a new document. This collapses a common 'find-then-insert-or-update' pattern into a single atomic operation — no race conditions, no extra round-trips.

For bulk changes, updateMany() applies your update to every document matching the filter. Just make sure your filter is tight. Running updateMany({}, { $set: { archived: true } }) marks every single document in the collection as archived — no confirmation prompt, no undo.

updateOrders.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const { MongoClient, ObjectId } = require('mongodb');

async function updateOrders() {
  const client = new MongoClient('mongodb://localhost:27017');

  try {
    await client.connect();
    const ordersCollection = client.db('storefront').collection('orders');

    // --- updateOne with $set: change a single order's status ---
    // ALWAYS use $set — without it, your whole document gets replaced
    const targetOrderId = new ObjectId('64f3a2b1c9e77f001a3d8e12');

    const statusUpdateResult = await ordersCollection.updateOne(
      { _id: targetOrderId },                         // filter: match by exact _id
      {
        $set: {
          orderStatus: 'dispatched',
          dispatchedAt: new Date()                    // add a new field on the fly — no schema migration needed
        }
      }
    );

    console.log('Documents matched:', statusUpdateResult.matchedCount);   // how many matched the filter
    console.log('Documents modified:', statusUpdateResult.modifiedCount); // how many were actually changed

    // --- $inc: atomically decrement stock without a read-modify-write ---
    // This is safe under concurrent writes; plain read+write is NOT
    const inventoryCollection = client.db('storefront').collection('inventory');

    await inventoryCollection.updateOne(
      { productSku: 'SHOE-RED-42' },
      { $inc: { stockCount: -1 } }                   // subtract 1 from stockCount atomically
    );

    // --- upsert: create a shipment record if it doesn't exist, update if it does ---
    const shipmentResult = await client.db('storefront').collection('shipments').updateOne(
      { orderId: targetOrderId },                    // filter: does a shipment for this order exist?
      {
        $set: {
          orderId: targetOrderId,
          carrier: 'FastFreight',
          trackingNumber: 'FF-993821-XZ',
          estimatedDelivery: new Date('2024-09-05')
        }
      },
      { upsert: true }                               // create it if it doesn't exist
    );

    // upsertedId is non-null only when a new document was created
    if (shipmentResult.upsertedId) {
      console.log('Shipment record created with ID:', shipmentResult.upsertedId);
    } else {
      console.log('Existing shipment record updated');
    }

    // --- updateMany: mark all orders older than 30 days as 'archived' ---
    const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

    const archiveResult = await ordersCollection.updateMany(
      { createdAt: { $lt: thirtyDaysAgo }, orderStatus: 'dispatched' }, // tight filter!
      { $set: { orderStatus: 'archived', archivedAt: new Date() } }
    );

    console.log(`Archived ${archiveResult.modifiedCount} old orders`);

  } finally {
    await client.close();
  }
}

updateOrders().catch(console.error);
Output
Documents matched: 1
Documents modified: 1
Shipment record created with ID: 64f3a2b1c9e77f001a3d9f01
Archived 0 old orders
Watch Out: Update Without $set Replaces the Document
updateOne({ _id: id }, { orderStatus: 'dispatched' }) doesn't add a field — it replaces the entire document with { orderStatus: 'dispatched' }. You'll lose every other field silently. Always wrap your changes in { $set: { ... } }.
Production Insight
Forgetting $set is the leading cause of accidental data loss in MongoDB. It's silent — no error, just missing data.
$inc is atomic; separate read-modify-write is not safe under concurrency.
Rule: if you need to update multiple documents with different values, consider bulkWrite with multiple updateOne statements.
Key Takeaway
Always wrap update fields in an operator: $set, $inc, $push, etc.
Use upsert:true to avoid separate find-then-insert race conditions.
Check matchedCount and modifiedCount to confirm the update had intended effect.

Delete — Removing Data Safely and Intentionally

Deletion in MongoDB is permanent and instantaneous. There's no recycle bin, no soft-delete built in, and no ROLLBACK. This is why production teams almost universally implement soft-deletes — adding a deletedAt timestamp field and filtering it out of queries — rather than physically removing documents. Physical deletion is reserved for true cleanup jobs like purging GDPR-expired data or clearing test fixtures.

MongoDB gives you deleteOne() for surgical removal of a single document and deleteMany() for bulk removal. The same golden rule from updates applies: if your filter is too broad, you will delete more than you intended. Always test your filter with a find() call first — confirm the count and spot-check a few returned documents before converting it to a deleteMany().

findOneAndDelete() is the atomic 'grab it and kill it' operation. It deletes the document and returns it to your application in a single server-side operation. This is exactly what you need for job queue patterns where a worker claims a task — using separate find() then deleteOne() calls creates a race condition where two workers could claim the same job.

Never run deleteMany({}) in production without a filter. There's no faster way to have a very bad day.

deleteOrders.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
const { MongoClient, ObjectId } = require('mongodb');

async function deleteOrders() {
  const client = new MongoClient('mongodb://localhost:27017');

  try {
    await client.connect();
    const db = client.db('storefront');
    const ordersCollection = db.collection('orders');

    // --- PATTERN 1: Soft delete (recommended for business data) ---
    // Don't physically remove — mark as deleted so you keep the audit trail
    const orderToSoftDelete = new ObjectId('64f3a2b1c9e77f001a3d8e13');

    await ordersCollection.updateOne(
      { _id: orderToSoftDelete },
      {
        $set: {
          isDeleted: true,
          deletedAt: new Date()    // lets you audit WHEN it was deleted and query "deleted in last 30 days"
        }
      }
    );
    console.log('Order soft-deleted (record preserved for audit)');

    // --- PATTERN 2: Hard delete with deleteOne ---
    // Appropriate for test data, temp records, or GDPR erasure requests
    const tempOrderId = new ObjectId('64f3a2b1c9e77f001a3d8e14');

    const hardDeleteResult = await ordersCollection.deleteOne({ _id: tempOrderId });
    console.log('Hard-deleted document count:', hardDeleteResult.deletedCount);
    // deletedCount will be 0 if the _id didn't exist — not an error, just a miss

    // --- PATTERN 3: findOneAndDelete — atomic claim-and-remove for job queues ---
    const jobsCollection = db.collection('pendingEmailJobs');

    // Grab the highest-priority job AND remove it in one atomic step
    // If two workers call this simultaneously, only one gets the document
    const claimedJob = await jobsCollection.findOneAndDelete(
      { status: 'queued' },
      {
        sort: { priority: -1, queuedAt: 1 },  // highest priority, then FIFO
        returnDocument: 'before'              // return the document as it was before deletion
      }
    );

    if (claimedJob) {
      console.log('Worker claimed job:', claimedJob.jobId, '| Recipient:', claimedJob.recipientEmail);
    } else {
      console.log('No jobs in queue right now');
    }

    // --- PATTERN 4: deleteMany for bulk cleanup (always preview first!) ---
    // Step 1: preview — what would get deleted?
    const toDeleteCount = await ordersCollection.countDocuments({
      isDeleted: true,
      deletedAt: { $lt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) } // older than 90 days
    });
    console.log(`About to permanently purge ${toDeleteCount} soft-deleted orders...`);

    // Step 2: execute only when you're sure
    const purgeResult = await ordersCollection.deleteMany({
      isDeleted: true,
      deletedAt: { $lt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) }
    });
    console.log('Purged:', purgeResult.deletedCount, 'old orders');

  } finally {
    await client.close();
  }
}

deleteOrders().catch(console.error);
Output
Order soft-deleted (record preserved for audit)
Hard-deleted document count: 1
No jobs in queue right now
About to permanently purge 0 soft-deleted orders...
Purged: 0 old orders
Pro Tip: Preview Before deleteMany
Run countDocuments() with the exact same filter before any deleteMany() call. It's a two-second habit that has saved entire collections from accidental wipes. Treat deleteMany like a controlled demolition — measure twice, delete once.
Production Insight
Physical deletes are permanent; there is no rollback without a backup. Soft-deletes give you a recovery window.
findOneAndDelete is the only safe way to implement job queues — separate find and delete creates races.
Rule: preview your filter with countDocuments before any deleteMany.
Key Takeaway
Use soft-deletes for business-critical data.
Use findOneAndDelete for atomic claim-and-remove.
Always test the filter on a find before running deleteMany.

Write Concerns, Error Handling, and Retry Logic

Production applications need to decide how durable their writes are. MongoDB's writeConcern setting controls the level of acknowledgment: w:1 (acknowledge from primary only), w:majority (ack from replica set majority), or j:true (journaled). Each level trades latency for durability. If you absolutely cannot lose a write — say a payment confirmation — use w:majority and j:true. For logs or transient data, w:1 is fine.

Errors happen. Duplicate key errors (E11000) are thrown when a unique index constraint is violated. Your code must catch them. Network timeouts and writeConcern timeouts are distinct: the former means the driver couldn't reach the server, the latter means the server couldn't gather enough acknowledgments in time. Both require retry logic. The MongoDB driver provides built-in retryable writes for network errors, but not for writeConcern errors. You'll need to implement exponential backoff yourself.

A robust write pattern: attempt the write, catch errors, inspect the error label to distinguish transient from permanent, retry transient errors with backoff, and log permanent errors for later investigation. Never swallow errors — a failed write that goes unnoticed becomes a silent data loss.

writeWithRetry.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { MongoClient } = require('mongodb');

async function insertWithRetry(collection, doc, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      // Set high writeConcern for critical data
      const result = await collection.insertOne(doc, {
        writeConcern: { w: 'majority', j: true }
      });
      return result;
    } catch (err) {
      // Differentiate transient vs permanent
      if (err.code === 11000) {
        // Duplicate key — permanent, log and abort
        console.error('Duplicate key for doc:', JSON.stringify(doc));
        throw err;  // Re-throw for caller to handle
      }
      if (err.hasErrorLabel('TransientTransactionError') || err.message.includes('timeout')) {
        // Transient — retry with backoff
        if (attempt === maxRetries) {
          console.error('Max retries reached, write failed');
          throw err;
        }
        const delay = Math.pow(2, attempt) * 100;  // 200ms, 400ms, 800ms
        console.log(`Retrying write, attempt ${attempt + 1} after ${delay}ms`);
        await new Promise(resolve => setTimeout(resolve, delay));
      } else {
        // Other permanent error
        throw err;
      }
    }
  }
}
Beware the Silent WriteConcern Timeout
A writeConcern timeout does not mean the write failed — it means the acknowledged count wasn't confirmed in time. The write may still succeed on the primary. Use retry logic with idempotent operations to avoid duplicate inserts on timeout.
Production Insight
Read-preference and writeConcern decisions affect both consistency and latency; test under production-like load.
Retryable writes from the driver handle only network errors — not writeConcern timeouts or duplicate keys.
Rule: implement your own retry with exponential backoff for custom error types.
Key Takeaway
Choose writeConcern based on data criticality: w:majority for payments, w:1 for logs.
Handle duplicate key errors explicitly — they're not edge cases, they're design constraints.
Retry transient errors with backoff; log permanent errors without re-throwing silently.
● Production incidentPOST-MORTEMseverity: high

Accidental Bulk Update Wipes Fields on 10k Orders

Symptom
Orders showed only the field that was updated; all other fields missing.
Assumption
The updateOne call with a plain document would only change the specified field.
Root cause
updateOne with a plain document as second argument replaces the whole document; $set is required for partial update.
Fix
Add $set operator to the update call; restore from backup for affected documents.
Key lesson
  • Always use $set for partial updates; a single-line omit can cost hours of data recovery.
  • Write a small script to validate the update with findOne before executing on production.
Production debug guideQuick reference for common MongoDB query failures in production4 entries
Symptom · 01
Query returns zero results even though document exists
Fix
Check that _id is compared with correct type (string vs ObjectId). Use typeof filter._id; if string, convert with new ObjectId().
Symptom · 02
find() returns cursor metadata instead of documents
Fix
Chain .toArray() or use for await...of. Logging find() without materialisation prints cursor info, not data.
Symptom · 03
Update doesn't change any documents
Fix
Check matchedCount vs modifiedCount in result object. If matchedCount is 0, filter misses. If modifiedCount is 0, document already matches intended state.
Symptom · 04
insertMany fails validation on first doc and skips rest
Fix
Set {ordered: false} to continue inserting valid documents. Check result.writeErrors for details of failed docs.
★ MongoDB CRUD Quick Debug Cheat SheetThree common production failures and the exact steps to diagnose and fix them
No results when querying by _id
Immediate action
Check the type of the _id value in your filter
Commands
typeof filter._id
If string, convert: new ObjectId(filter._id)
Fix now
Use ObjectId constructor in the query
Update silently deletes fields+
Immediate action
Inspect the update argument for missing $set operator
Commands
console.log('Update argument:', update);
If it's a plain object, wrap in { $set: update }
Fix now
Restore from backup; add $set to all future updates
insertMany aborts early on first error+
Immediate action
Add { ordered: false } option
Commands
db.collection.insertMany(docs, { ordered: false })
Check result.writeErrors array
Fix now
Change ordered default to false for bulk imports
OperationSingle Document MethodMultiple Documents MethodAtomic Grab-and-Act
CreateinsertOne(doc)insertMany([docs], {ordered:false})N/A
ReadfindOne(filter, options)find(filter).limit(n).toArray()N/A
UpdateupdateOne(filter, {$set:{...}})updateMany(filter, {$set:{...}})findOneAndUpdate()
DeletedeleteOne(filter)deleteMany(filter)findOneAndDelete()
Upsert supportYes — {upsert:true} optionYes — {upsert:true} optionYes — {upsert:true} option
Returns modified docNo — returns result metadataNo — returns result metadataYes — returns document
Race-condition safeNot inherentlyNot inherentlyYes — single atomic op

Key takeaways

1
Always use update operators like $set and $inc
passing a plain object as the update argument replaces the whole document silently, which is almost never what you want.
2
find() returns a lazy Cursor, not an array
always chain .toArray() or use for await...of, and always add .limit() to open-ended queries before they hit production traffic.
3
findOneAndDelete() and findOneAndUpdate() aren't just convenience methods
they're the only way to atomically claim-and-act on a document without race conditions between concurrent processes.
4
Soft-deletes (adding isDeleted
true and deletedAt fields) preserve your audit trail and make GDPR compliance practical. Reserve physical deletion for temp data, test fixtures, and scheduled purge jobs.
5
Choose writeConcern based on data criticality
w:majority for payments, w:1 for logs. Write idempotent operations and implement retry logic for transient errors.

Common mistakes to avoid

4 patterns
×

Updating without $set

Symptom
Document silently replaced; all fields except the update values are lost.
Fix
Always wrap your changes in a $set (or $inc, $push) operator. Example: updateOne({ _id: id }, { $set: { status: 'active' } }).
×

Comparing string IDs to ObjectId

Symptom
Queries return zero results with no error because the types don't match.
Fix
Wrap the ID string in new ObjectId('...') before using in filter. Always ensure _id field is compared as ObjectId.
×

Using deprecated count() instead of countDocuments()

Symptom
Inaccurate document counts on sharded clusters, leading to pagination bugs.
Fix
Always use countDocuments(filter) which scans live data and respects filters. Avoid count() entirely.
×

Not using projection in reads

Symptom
Massive network bandwidth waste and slow serialization from fetching all fields.
Fix
Always specify projection in find() and findOne() options. Pro tip: exclude _id if not needed.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What's the difference between updateOne() and replaceOne() in MongoDB, a...
Q02SENIOR
How would you implement an atomic 'claim a task from a queue' operation ...
Q03SENIOR
If countDocuments() and estimatedDocumentCount() both count documents, w...
Q01 of 03SENIOR

What's the difference between updateOne() and replaceOne() in MongoDB, and when would you deliberately choose replaceOne()?

ANSWER
updateOne modifies specific fields using operators like $set, while replaceOne replaces the entire document with a new one. You'd choose replaceOne when you want to change the document's structure entirely — for example, after a GDPR data erasure request where you need to replace a user document with a minimal anonymised version, keeping only the _id.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between insertOne and insertMany in MongoDB?
02
Why does my MongoDB updateOne() query delete all my document fields?
03
Does MongoDB have transactions like SQL databases?
04
What is writeConcern and how does it affect durability?
🔥

That's NoSQL. Mark it forged?

5 min read · try the examples if you haven't

Previous
MongoDB Basics
3 / 15 · NoSQL
Next
MongoDB Aggregation Pipeline