🍃 CRUD Operations in MongoDB

MongoDB is a document-oriented NoSQL database that stores data as flexible, JSON-like documents called BSON. At the heart of every MongoDB application lies CRUD — Create, Read, Update, and Delete — the four fundamental operations used to interact with data. This tutorial walks through each operation in detail, from beginner basics to advanced patterns, using the official MongoDB Node.js Driver.

Information

This tutorial assumes you have a running MongoDB instance (local or Atlas) and a basic understanding of JavaScript.

📖 1. Introduction

Every database interaction ultimately boils down to one of four operations: creating new data, reading existing data, updating data, or deleting data. MongoDB provides a rich, expressive API for each of these operations, allowing developers to work with data in a natural, JavaScript-friendly way.

CRUD
➕ Create
🔍 Read
âœī¸ Update
đŸ—‘ī¸ Delete

🧩 2. CRUD Overview

Each CRUD operation in MongoDB is exposed through methods on a Collection object. Before performing any operation, you must first connect to a MongoDB instance and select a database and collection.

Connecting to MongoDB

const { MongoClient } = require("mongodb");

const client = new MongoClient("mongodb://localhost:27017");

async function main() {
  await client.connect();
  const db = client.db("shop");
  const collection = db.collection("products");
  // CRUD operations go here
}

main().catch(console.error);
OperationPurposeCommon Methods
CreateInsert new documentsinsertOne(), insertMany()
ReadQuery existing documentsfind(), findOne()
UpdateModify existing documentsupdateOne(), updateMany()
DeleteRemove documentsdeleteOne(), deleteMany()

➕ 3. Creating Documents

The Create operation adds new documents to a collection. MongoDB automatically generates a unique _id field for each document unless you explicitly provide one.

insertOne()

Use insertOne() to insert a single document into a collection.

Inserting a single document

const result = await collection.insertOne({
  name: "Wireless Mouse",
  price: 25.99,
  inStock: true
});

console.log(result.insertedId);

Tip

The returned result object contains an insertedId field, which is the _id of the newly created document.

insertMany()

Use insertMany() to insert multiple documents in a single, efficient database call.

Inserting multiple documents

const result = await collection.insertMany([
  { name: "Keyboard", price: 49.99 },
  { name: "Monitor", price: 199.99 },
  { name: "Webcam", price: 39.99 }
]);

console.log(result.insertedCount);

Warning

By default, insertMany() stops on the first error (ordered inserts). Pass { ordered: false } to continue inserting remaining documents even if one fails.

🔍 6. Reading Documents

The Read operation retrieves documents from a collection based on specified criteria. MongoDB's query language is flexible, supporting exact matches, comparisons, and complex logical conditions.

find()

find() returns a cursor pointing to all documents matching a query. An empty filter {} returns every document in the collection.

Finding all matching documents

const cursor = collection.find({ inStock: true });
const products = await cursor.toArray();

console.log(products);

findOne()

findOne() returns the first matching document only, or null if no document matches.

Finding a single document

const product = await collection.findOne({ name: "Keyboard" });
console.log(product);

đŸŽ¯ 9. Query Filters

Query filters use comparison and logical operators to narrow down results. Filters are written as objects where keys map to fields and values describe the matching criteria.

OperatorMeaningExample
$eqEqual to{ price: { $eq: 25 } }
$gt / $gteGreater than / or equal{ price: { $gt: 50 } }
$lt / $lteLess than / or equal{ price: { $lt: 100 } }
$inMatches any value in an array{ name: { $in: ["Mouse","Keyboard"] } }
$and / $orLogical combinations{ $or: [{ inStock: true }, { price: { $lt: 10 } }] }

Combining multiple filter conditions

const results = await collection.find({
  price: { $gte: 20, $lte: 100 },
  inStock: true
}).toArray();

đŸ—‚ī¸ 10. Projection

Projection controls which fields are returned in query results, reducing network overhead by excluding unnecessary data.

Applying a projection

// Include only name and price, exclude _id
const results = await collection
  .find({}, { projection: { name: 1, price: 1, _id: 0 } })
  .toArray();

Note

You cannot mix inclusion (1) and exclusion (0) in the same projection, except for the _id field.

â†•ī¸ 11. Sorting

The sort() method orders results by one or more fields. Use 1 for ascending order and -1 for descending order.

Sorting by price (descending)

const results = await collection
  .find({})
  .sort({ price: -1 })
  .toArray();

đŸ”ĸ 12. Limiting Results

limit() restricts the number of documents returned, which is especially useful for pagination.

Limiting query results

const topThree = await collection
  .find({})
  .sort({ price: -1 })
  .limit(3)
  .toArray();

â­ī¸ 13. Skipping Results

skip() bypasses a specified number of documents, commonly combined with limit() to implement paginated queries.

Paginating with skip() and limit()

const page2 = await collection
  .find({})
  .sort({ _id: 1 })
  .skip(10)
  .limit(10)
  .toArray();

Caution

skip() becomes inefficient on large collections since MongoDB must still scan and discard skipped documents. Consider cursor-based pagination for large datasets.

đŸ”ĸ 14. Counting Documents

Use countDocuments() to count how many documents match a given filter.

Counting matching documents

const count = await collection.countDocuments({ inStock: true });
console.log(`In-stock products: ${count}`);

âœī¸ 15. Updating Documents

The Update operation modifies existing documents. MongoDB uses update operators (prefixed with $) to describe precisely how a document should change.

updateOne()

Updates the first document that matches the filter.

Updating a single document

const result = await collection.updateOne(
  { name: "Mouse" },
  { $set: { price: 22.99 } }
);

console.log(result.modifiedCount);

updateMany()

Updates all documents matching the filter.

Updating multiple documents

const result = await collection.updateMany(
  { inStock: false },
  { $set: { discontinued: true } }
);

console.log(result.modifiedCount);

replaceOne()

Unlike updateOne(), which modifies specific fields, replaceOne() replaces the entire document (except _id) with a new one.

Replacing a document entirely

const result = await collection.replaceOne(
  { name: "Webcam" },
  { name: "Webcam Pro", price: 59.99, inStock: true }
);

Important

Any fields not included in the replacement document will be removed from the original document.

đŸ› ī¸ 19. Update Operators

OperatorDescription
$setSets the value of a field
$unsetRemoves a field
$incIncrements a numeric field
$pushAppends a value to an array
$pullRemoves a value from an array
$renameRenames a field

Using multiple update operators together

await collection.updateOne(
  { name: "Mouse" },
  {
    $inc: { stockCount: -1 },
    $push: { tags: "electronics" }
  }
);

đŸ—‘ī¸ 20. Deleting Documents

The Delete operation removes documents that match a given filter. Deleted documents cannot be recovered unless backups exist.

deleteOne()

Deleting a single document

const result = await collection.deleteOne({ name: "Webcam Pro" });
console.log(result.deletedCount);

deleteMany()

Deleting multiple documents

const result = await collection.deleteMany({ discontinued: true });
console.log(result.deletedCount);

Danger

Calling deleteMany({}) with an empty filter deletes every document in the collection. Always double-check your filter before running delete operations in production.

âš™ī¸ 23. Bulk Operations

bulkWrite() allows you to combine multiple insert, update, and delete operations into a single database round-trip, improving performance for batch workloads.

Combining operations with bulkWrite()

const result = await collection.bulkWrite([
  { insertOne: { document: { name: "USB Hub", price: 15.99 } } },
  { updateOne: {
      filter: { name: "Mouse" },
      update: { $set: { price: 19.99 } }
    }
  },
  { deleteOne: { filter: { name: "Old Item" } } }
]);

console.log(result);

🔁 24. Upserts

An upsert (update + insert) creates a new document if no document matches the filter, or updates the existing one if a match is found. Enable this behavior with the upsert option.

Using upsert to insert-or-update

const result = await collection.updateOne(
  { sku: "SKU-1001" },
  { $set: { name: "Desk Lamp", price: 29.99 } },
  { upsert: true }
);

if (result.upsertedId) {
  console.log("New document created:", result.upsertedId);
}

Hint

Upserts are especially useful for idempotent data pipelines, where re-running the same operation should not create duplicate documents.

🚨 25. Error Handling

Robust applications must handle errors gracefully — including connection failures, validation errors, and duplicate key violations.

Handling duplicate key errors

try {
  await collection.insertOne({ _id: 1, name: "Duplicate" });
  await collection.insertOne({ _id: 1, name: "Duplicate" });
} catch (error) {
  if (error.code === 11000) {
    console.error("Duplicate key error:", error.message);
  } else {
    console.error("Unexpected error:", error);
  }
}

Example

Error code 11000 indicates a duplicate key violation, typically triggered when inserting a document with an existing unique index value.

🌟 26. Best Practices

  • Always use projection to fetch only the fields you actually need.
  • Create appropriate indexes on frequently queried fields to improve performance.
  • Prefer updateOne()/updateMany() over replaceOne() for partial updates.
  • Use bulkWrite() for batch operations instead of looping individual calls.
  • Always validate and sanitize user input before using it in a query filter.
  • Use transactions when multiple operations must succeed or fail together.

âš ī¸ 27. Common Mistakes

  1. Forgetting the filter in deleteMany() or updateMany(), accidentally affecting every document.
  2. Using replaceOne() when only a partial update (updateOne()) was intended, causing data loss.
  3. Not indexing fields used in frequent queries, leading to slow find() operations.
  4. Ignoring the result.matchedCount vs result.modifiedCount distinction when debugging updates.

Best Practice

Before running any updateMany() or deleteMany() in production, first run the equivalent find() query to confirm exactly which documents will be affected.

❓ 28. Frequently Asked Questions

No. If you don't provide an _id field, MongoDB automatically generates a unique ObjectId for the document.

Yes. find() returns a cursor that lazily fetches documents in batches as you iterate, rather than loading the entire result set into memory at once.

No. The _id field is immutable once set. Attempting to modify it will result in an error.

📌 29. Summary

>>"Data is the new oil, but like oil, it needs proper handling — CRUD operations are the refinery."

Summary

Mastering CRUD operations is the foundation of working with MongoDB. Once comfortable with these basics, explore transactions, the aggregation framework, and indexing strategies to build production-grade applications.