Skip to content

CRUD in MongoDB

Every database boils down to four things you can do with your data: put new records in, read them back, change them, and throw them away. People shorten those four verbs to CRUD — Create, Read, Update, Delete. In MongoDB the “records” are JSON-like documents living inside a collection, and the verbs map onto a small, predictable set of methods that look almost identical whether you type them into the shell or call them from a driver in Node.js, Python, Go, or Rust.

This module is the working heart of the course. By the end of it you will be able to load documents into a collection, pull them back out with filters, surgically change fields without clobbering the rest of a document, remove exactly what you mean to remove, and combine many writes into a single efficient round trip. We will keep the data deliberately small and made-up — a handful of imaginary library members and the books they borrow — so the operations stay in the spotlight.

flowchart LR
  App["Your app or mongosh"] -->|"insert / find / update / delete"| Driver["Driver or shell"]
  Driver --> Coll["Collection: members"]
  Coll --> D1["{ _id, name, fines }"]
  Coll --> D2["{ _id, name, fines }"]
  Coll --> D3["{ _id, name, fines }"]
A CRUD call travels from your code, through the driver, into a collection of documents

Each family has a singular method that touches at most one document and a plural method that can touch many. Here is the whole vocabulary you will spend this module learning:

  • Create — insertOne adds a single document, insertMany adds an array of them.
  • Read — findOne returns the first matching document (or nothing), find returns a cursor over every match.
  • Update — updateOne changes the first match, updateMany changes every match. Both use update operators like $set and $inc.
  • Delete — deleteOne removes the first match, deleteMany removes every match.

The same shape shows up across all five tabs you will see throughout this module. Watch how an insert reads almost the same in the shell and in each driver:

mongosh/crud-insert-tab.js
db.tabs.insertOne({ name: "Ada", fines: 0 })
assert.strictEqual(db.tabs.countDocuments(), 1)

Every CRUD call answers with an acknowledgement, and reading it is how you confirm what happened. This fence runs one call of each family in mongosh and prints the fields each acknowledgement carries (mongo 8.3.11, mongosh 2.11.1, this Mac):

mongosh/crud-acks.js
const keys = (o) => Object.keys(o).join(", ");
console.log("insertOne :", keys(db.members.insertOne({ name: "Ada", fines: 0 })));
console.log("insertMany:", keys(db.members.insertMany([{ name: "Grace", fines: 2 }, { name: "Linus", fines: 5 }])));
console.log("updateOne :", keys(db.members.updateOne({ name: "Ada" }, { $set: { fines: 1 } })));
console.log("upsert :", keys(db.members.updateOne({ name: "Nina" }, { $set: { fines: 0 } }, { upsert: true })));
console.log("deleteOne :", keys(db.members.deleteOne({ name: "Nina" })));
console.log("bulkWrite :", keys(db.members.bulkWrite([{ deleteOne: { filter: { name: "Nobody" } } }])));
console.log("find :", db.members.find({ fines: { $gt: 0 } }).toArray().length, "documents");
assert.strictEqual(db.members.countDocuments(), 3);
insertOne : acknowledged, insertedId
insertMany: acknowledged, insertedIds
updateOne : acknowledged, insertedId, matchedCount, modifiedCount, upsertedCount
upsert : acknowledged, insertedId, matchedCount, modifiedCount, upsertedCount
deleteOne : acknowledged, deletedCount
bulkWrite : acknowledged, insertedCount, insertedIds, matchedCount, modifiedCount, deletedCount, upsertedCount, upsertedIds
find : 3 documents

Remember the _id that MongoDB added for you: insertOne({ name: "Ada", fines: 0 }) stores { _id: ObjectId('...'), name: 'Ada', fines: 0 }. (The Node driver shapes these results a little differently - insertedCount on insertMany, upsertedId on updates - the individual lessons show both.)

In Compass: the same four families map to the GUI - the Documents tab inserts (Add Data > Insert Document), edits (pencil icon on hover, then Update) and deletes (trash icon on hover); the individual lessons cite the Compass documentation for each.

The lessons that follow take each family in turn, then finish with the two power moves that combine families:

  1. Insert — adding one or many documents, and the automatic _id.
  2. Read — find versus findOne, equality and multi-field filters, and a first taste of projection.
  3. Update — $set, $inc, $unset, $push, $pull, and reading the match counts a write returns.
  4. Delete — removing documents precisely, plus the soft-delete pattern.
  5. Upsert and bulk writes — “insert if missing, otherwise update” and packing many operations into one trip to the server.
  • The singular methods (insertOne, findOne, updateOne, deleteOne) never affect more than one document, even if many match — handy as a safety rail.
  • Write methods return an acknowledgement describing what happened (counts, inserted ids). Reading that result is how you confirm an operation did what you intended.
  • A filter that matches nothing is not an error. An update or delete that matches zero documents simply reports zero changes.

CRUD code from an assistant almost always ends without looking at the result:

🐛 Spot the bug

// AI comment: "mark Ada's fines as paid"
async function markPaid(db, name) {
await db.collection("members").updateOne({ nmae: name }, { $set: { fines: 0 } });
return "ok";
}
Show the answer

There is a typo in the filter (nmae). MongoDB does not know that nmae is not a field, so it looks for documents with a field called nmae, finds none, changes nothing - and reports success: { acknowledged: true, matchedCount: 0, modifiedCount: 0 }. The function returns "ok" and Ada still owes her fines. The fix is to read the acknowledgement:

app/crud-check-result.mjs
const members = db.collection("members");
await members.insertOne({ name: "Ada", fines: 3 });
async function markPaid(name, field) {
const res = await members.updateOne({ [field]: name }, { $set: { fines: 0 } });
if (res.matchedCount === 0) throw new Error(`no member matched { ${field}: "${name}" }`);
return res.modifiedCount === 1 ? "paid" : "already zero";
}
let typo = null;
try { await markPaid("Ada", "nmae"); } catch (e) { typo = e.message; }
console.log({ typo, ok: await markPaid("Ada", "name"), again: await markPaid("Ada", "name") });
if (!typo) throw new Error("the typo should have been caught");
{ typo: 'no member matched { nmae: "Ada" }', ok: 'paid', again: 'already zero' }

A collection is a bag of documents; every CRUD call is filter + action. The filter selects (nothing selected is not an error), the action inserts, reads, changes or removes, and the acknowledgement tells you how many documents each step touched. When the count is not what you expected, the filter is almost always the culprit.

ChoiceBenefitCost
Singular methods (insertOne/updateOne/deleteOne)Precise control, affects one document, easy to predictMust be called repeatedly to handle many documents, causing frequent round trips
Plural methods (insertMany/updateMany/deleteMany)Handles many documents in one call, much fasterA mistaken filter or array affects a much wider scope than intended; not atomic across documents
Hard deleteReclaims storage immediately, simplestPermanent, no audit trail
Soft delete (update a flag)Recoverable, has an audit trailMust filter the flag out of every subsequent query
  • “A write that reports no error succeeded.” - the typo’d filter above returned matchedCount: 0 and no error. Check the counts.
  • “insertMany / updateMany are transactions.” - each document write is atomic, the batch is not (the CRUD manual: “All write operations in MongoDB are atomic on the level of a single document”).
  • “Looping one insert per document is fine.” - batch with insertMany (or bulkWrite); the lessons measure it.
  • “updateOne(filter, { plain: 'doc' }) replaces the document.” - modern drivers throw; replaceOne replaces (update lesson).
  • “The shell and drivers return identical acknowledgements.” - the fields differ (insertedId vs upsertedId, insertedCount on insertMany).

Real-world example

The MongoDB manual’s CRUD page organises the same families - insertOne/insertMany, find, updateOne/updateMany/replaceOne, deleteOne/deleteMany, bulk write - and states that “All write operations in MongoDB are atomic on the level of a single document” (CRUD operations, consulted 2026-09-30).

Python/Go/Rust tabs in this module mirror the verified shell/Node behaviour and were each run once in scratch programs (see each lesson); they are not executed by the course’s automated checker.

Which method reads back the first document that matches a filter?
What does the singular form of a write method (like updateOne) guarantee?
CRUD stands for which four operations?
updateOne({ nmae: "Ada" }, { $set: { fines: 0 } }) has a typo in the field name. What is the result?
Which acknowledgement fields does deleteOne return in mongosh?