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 }"] The four operation families
Section titled “The four operation families”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 —
insertOneadds a single document,insertManyadds an array of them. - Read —
findOnereturns the first matching document (or nothing),findreturns a cursor over every match. - Update —
updateOnechanges the first match,updateManychanges every match. Both use update operators like$setand$inc. - Delete —
deleteOneremoves the first match,deleteManyremoves 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:
db.tabs.insertOne({ name: "Ada", fines: 0 })assert.strictEqual(db.tabs.countDocuments(), 1)await db.collection("members").insertOne({ name: "Ada", fines: 0 });if ((await db.collection("members").countDocuments()) !== 1) throw new Error("expected 1");db.members.insert_one({"name": "Ada", "fines": 0})coll.InsertOne(ctx, bson.M{"name": "Ada", "fines": 0})coll.insert_one(doc! { "name": "Ada", "fines": 0 }).await?;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):
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, insertedIdinsertMany: acknowledged, insertedIdsupdateOne : acknowledged, insertedId, matchedCount, modifiedCount, upsertedCountupsert : acknowledged, insertedId, matchedCount, modifiedCount, upsertedCountdeleteOne : acknowledged, deletedCountbulkWrite : acknowledged, insertedCount, insertedIds, matchedCount, modifiedCount, deletedCount, upsertedCount, upsertedIdsfind : 3 documentsRemember 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.
What this module covers
Section titled “What this module covers”The lessons that follow take each family in turn, then finish with the two power moves that combine families:
- Insert — adding one or many documents, and the automatic
_id. - Read —
findversusfindOne, equality and multi-field filters, and a first taste of projection. - Update —
$set,$inc,$unset,$push,$pull, and reading the match counts a write returns. - Delete — removing documents precisely, plus the soft-delete pattern.
- Upsert and bulk writes — “insert if missing, otherwise update” and packing many operations into one trip to the server.
Tips and gotchas
Section titled “Tips and gotchas”- 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.
Reading AI-generated code
Section titled “Reading AI-generated code”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:
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' }Mental Model
Section titled “Mental Model”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.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
| Singular methods (insertOne/updateOne/deleteOne) | Precise control, affects one document, easy to predict | Must be called repeatedly to handle many documents, causing frequent round trips |
| Plural methods (insertMany/updateMany/deleteMany) | Handles many documents in one call, much faster | A mistaken filter or array affects a much wider scope than intended; not atomic across documents |
| Hard delete | Reclaims storage immediately, simplest | Permanent, no audit trail |
| Soft delete (update a flag) | Recoverable, has an audit trail | Must filter the flag out of every subsequent query |
Common Misconceptions
Section titled “Common Misconceptions”- “A write that reports no error succeeded.” - the typo’d filter above returned
matchedCount: 0and 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(orbulkWrite); the lessons measure it. - “
updateOne(filter, { plain: 'doc' })replaces the document.” - modern drivers throw;replaceOnereplaces (update lesson). - “The shell and drivers return identical acknowledgements.” - the fields differ (
insertedIdvsupsertedId,insertedCountoninsertMany).
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.