A practical, developer-first guide to CRUD operations in MongoDB, covering insert, find, update, and delete methods with real query patterns, indexing tips, and production-safe practices.
CRUD Operations in MongoDB
MongoDB stores data as flexible BSON documents inside collections, which changes how create, read, update, and delete operations behave compared to relational tables. If you learned SQL first, the mental shift is the hardest part: there are no rows to lock into a fixed schema, no JOIN-first thinking, and updates operate on document fields using dedicated operators. This guide walks through every CRUD operation with the exact method names, argument shapes, and behavioral details that break real applications when misunderstood.

Quick Answer: CRUD operations in MongoDB are the four core database actions: Create with insertOne and insertMany, Read with find and findOne, Update with updateOne, updateMany, and replaceOne, and Delete with deleteOne and deleteMany. Each accepts a filter document, uses BSON field operators, and returns an acknowledgment object confirming matched and modified counts.
What CRUD Means in a Document Database
CRUD stands for Create, Read, Update, and Delete, the four operations that cover almost every database interaction an application performs. In MongoDB, all four run against a collection, which is a group of documents that do not need identical fields. A document is a BSON object, a binary-encoded superset of JSON that adds types SQL developers expect, including 64-bit integers, dates, decimals, and ObjectId.
That flexibility has a direct consequence for CRUD design: correctness moves from the schema into your queries and validation layer. MongoDB supports JSON Schema validation at the collection level, and since version 5.0 the driver-level write concern defaults to majority acknowledgment, meaning a write returns only after it reaches a majority of replica set members. Both features matter because they define whether your create and update calls are actually durable.
Create: insertOne and insertMany
MongoDB offers exactly two insert methods, and picking the wrong one is the most common source of slow write paths.

- insertOne(document, options) writes a single document and returns an object containing acknowledged and insertedId.
- insertMany(documents, options) writes an array of documents in one round trip and returns insertedIds keyed by array position.
A concrete example using the Node.js driver:
await db.collection("orders").insertOne({ userId: 42, total: 129.99, status: "pending", createdAt: new Date() });
Three behaviors are worth committing to memory. First, if you omit the _id field, MongoDB generates a 12-byte ObjectId that embeds a timestamp, so documents are roughly sortable by insertion time without an extra field. Second, insertMany runs in ordered mode by default, so it stops at the first error and leaves earlier documents inserted. Passing { ordered: false } continues past failures and is significantly faster for bulk loads because the server can parallelize. Third, inserting a duplicate _id throws a duplicate key error with code 11000, which is the error code you should catch explicitly rather than swallowing all write exceptions.
For batch ingestion, insertMany with ordered false typically moves 10,000 to 50,000 small documents per second on modest hardware, whereas looping insertOne is bounded by network round-trip latency and often lands under 2,000 per second on a remote cluster. The difference is not the database engine, it is the number of round trips.
Read: find, findOne, and Projections
Read operations in MongoDB take a filter document as the first argument and an optional projection as the second. The filter is a specification, not a string, which is why MongoDB queries resist classic SQL injection but remain vulnerable to operator injection when user input is passed directly into a filter object.

The practical read patterns you will use daily:
- findOne({ _id: id }) returns a single document or null.
- find({ status: "pending" }) returns a cursor, not an array, and must be iterated or converted with toArray().
- find(filter, { projection: { total: 1, status: 1 } }) returns only the listed fields plus _id, cutting network payload.
- Comparison operators include $gt, $gte, $lt, $lte, $ne, $in, and $nin, written as { total: { $gt: 100 } }.
- Logical operators include $and, $or, $nor, and $not, and $or takes an array of filter documents.
- Cursor modifiers sort, skip, and limit chain onto find and execute server side.
A critical detail most tutorials skip: skip based pagination degrades linearly because the server still walks the skipped documents. For any collection past roughly 100,000 documents, switch to range pagination using the last seen _id, for example find({ _id: { $gt: lastId } }).limit(20). The query stays constant time because it uses the index directly.
Always validate that user-supplied values are primitives before placing them in a filter. If a request body sends { "password": { "$ne": null } } and your code passes it straight into findOne, the filter matches any document. Casting to String or validating with a schema library eliminates the entire class of bug.
Update: Operators Are Mandatory
Update is where MongoDB diverges most sharply from SQL. You do not pass a new document, you pass an update document built from operators that describe the change.

The three update methods:
- updateOne(filter, update, options) modifies the first matching document.
- updateMany(filter, update, options) modifies every matching document.
- replaceOne(filter, replacement) swaps the entire document except _id.
The operators that cover most application logic:
| Operator | What It Does | Example |
|---|---|---|
| $set | Sets or creates a field value | { $set: { status: "paid" } } |
| $unset | Removes a field entirely | { $unset: { couponCode: "" } } |
| $inc | Atomically adds a number | { $inc: { views: 1 } } |
| $push | Appends to an array | { $push: { tags: "urgent" } } |
| $addToSet | Appends only if absent | { $addToSet: { tags: "urgent" } } |
| $pull | Removes matching array items | { $pull: { tags: "urgent" } } |
| $currentDate | Sets a field to server time | { $currentDate: { updatedAt: true } } |
Forgetting the operator wrapper is the single most frequent MongoDB mistake. Calling updateOne(filter, { status: "paid" }) throws an error because the driver expects operator keys, while replaceOne with that same object silently deletes every other field in the document. If you have ever seen production records lose their fields overnight, this is usually why.
Two options change update behavior meaningfully. The upsert: true option inserts the document if no match exists, which makes counters and idempotent syncs trivial. The findOneAndUpdate method returns the document itself, and passing returnDocument: "after" gives you the post-update state in a single atomic operation, avoiding the read-modify-write race that plagues naive implementations. Single-document updates in MongoDB are atomic regardless of how many fields they touch, so $inc is a safe counter primitive under heavy concurrency.
Delete: deleteOne, deleteMany, and Soft Deletes
Delete operations mirror the update signatures and take a filter as the first argument.

- deleteOne(filter) removes the first match and returns deletedCount.
- deleteMany(filter) removes all matches, and deleteMany({}) empties the collection while leaving indexes intact.
- drop() removes the collection and its indexes, which is far faster than deleting every document but destroys index definitions.
- findOneAndDelete(filter) removes a document and returns it, useful for queue-style workloads.
In my experience auditing production applications, hard deletes cause more incidents than they prevent. A soft delete pattern, setting { $set: { deletedAt: new Date() } } and filtering reads with { deletedAt: null }, preserves audit history and makes accidental deletion recoverable. Pair it with a TTL index on deletedAt so MongoDB purges records automatically after a retention window, which satisfies data retention requirements without a cron job. Teams building auditable systems often lean on experienced partners such as full stack development specialists to get retention and recovery rules right before launch rather than after a data loss event.
Making CRUD Fast: Indexes and Explain Plans
Every CRUD operation except a raw insert depends on how well your indexes match your filters. Without an index, MongoDB performs a collection scan, reading every document to evaluate the filter.

Four rules that consistently deliver results:
- Index the fields you filter and sort on. Create compound indexes in equality, sort, range order, so a query filtering status and sorting by createdAt wants { status: 1, createdAt: -1 }.
- Read the explain plan. Run find(filter).explain("executionStats") and check the winning stage. IXSCAN means index use, COLLSCAN means a full scan.
- Watch totalKeysExamined against nReturned. A healthy ratio is close to 1 to 1. A ratio above 10 to 1 signals a poorly shaped index.
- Do not over-index. Every index adds write cost to insert, update, and delete, because each index must be maintained on every write.
MongoDB documents have a 16MB size limit, and a single index entry cannot exceed 1024 bytes. Both limits shape data modeling: unbounded arrays inside a document eventually break updates, so high-growth relationships belong in a separate collection referenced by _id.
Common CRUD Mistakes That Reach Production

Most MongoDB bugs I see fall into five repeatable categories.
- Passing a string where an ObjectId is required. A filter of { _id: "64f1a2..." } silently matches nothing. Convert with new ObjectId(id) and wrap it in a try block because malformed input throws.
- Ignoring the result object. updateOne returns matchedCount and modifiedCount. A matchedCount of 0 means your filter failed, which is a different bug from a modifiedCount of 0, which means the values were already correct.
- Unbounded find calls. Always apply limit on user-facing reads so a single request cannot stream a million documents into memory.
- Assuming multi-document atomicity. Only single-document writes are atomic by default. Cross-document consistency requires an explicit transaction with a session, available on replica sets since MongoDB 4.0 and on sharded clusters since 4.2.
- Creating a client per request. The driver manages a connection pool internally. Instantiate one MongoClient and reuse it, or you will exhaust connection limits under load.
Production Checklist for MongoDB CRUD

Before shipping any CRUD layer, verify each item:
- Every filter field used in a hot query path has a supporting index.
- All user input is type-validated before entering a filter or update document.
- Write concern is set to majority for data that must survive a node failure.
- Bulk inserts use insertMany with ordered false where partial success is acceptable.
- Pagination uses range queries, not deep skip values.
- Duplicate key error 11000 is handled explicitly on unique fields.
- Deletes are soft where audit history matters, with a TTL index for cleanup.
- Connection pooling reuses a single client instance across requests.
Teams that formalize this checklist during code review catch the overwhelming majority of data layer defects before deployment. If you want an outside review of your data layer or a build handled end to end, WebPeak Digital works through exactly this kind of production hardening.
Key Takeaways
- MongoDB CRUD uses eight core methods: insertOne, insertMany, find, findOne, updateOne, updateMany, deleteOne, and deleteMany.
- Update operations require operators such as $set and $inc, and replaceOne overwrites every field except _id.
- Single-document writes are atomic; multi-document consistency requires an explicit transaction, supported on replica sets since MongoDB 4.0.
- Documents are capped at 16MB and index entries at 1024 bytes, which directly constrains embedding strategy.
- Range pagination with _id stays constant time, while skip pagination degrades linearly with offset size.
- explain("executionStats") is the authoritative way to confirm whether a query uses an index.
Frequently Asked Questions (FAQ)
What are the four CRUD operations in MongoDB?
The four CRUD operations are Create, Read, Update, and Delete. In MongoDB these map to insertOne and insertMany for creating documents, find and findOne for reading, updateOne, updateMany, and replaceOne for modifying, and deleteOne and deleteMany for removing documents from a collection.
What is the difference between updateOne and replaceOne in MongoDB?
updateOne applies update operators such as $set to change specific fields while leaving all other fields untouched. replaceOne swaps the entire document with the object you supply, keeping only the original _id. Using replaceOne when you meant updateOne silently deletes every field you did not include.
How do I update multiple documents at once in MongoDB?
Use updateMany with a filter and an operator-based update document, for example updateMany({ status: "pending" }, { $set: { status: "expired" } }). It modifies every matching document and returns matchedCount and modifiedCount so you can verify how many records the filter actually reached.
Are MongoDB CRUD operations atomic?
Every write against a single document is atomic in MongoDB, even when it updates many fields or nested arrays. Operations spanning multiple documents are not atomic by default and need an explicit multi-document transaction with a client session, supported on replica sets from MongoDB 4.0 onward.
Why does my MongoDB find query return no results?
The most frequent cause is passing a string where an ObjectId is expected, since { _id: "abc123" } never matches an ObjectId value. Convert it with new ObjectId(id). Other causes include field name typos, case sensitivity, and comparing a numeric string against a stored number.
How can I make MongoDB CRUD operations faster?
Index every field used in filters and sorts, build compound indexes in equality, sort, range order, and confirm index usage with explain("executionStats"). Replace loops of insertOne with insertMany, project only the fields you need, apply limit on reads, and avoid deep skip pagination.
