CRUD and querying
Insert and read documents
Use insertOne, insertMany, find, filters, projections, and ObjectId values without treating shell expressions as SQL.
9 minute lesson
Use insertOne() for one document and insertMany() for a batch. MongoDB adds an _id when you do not supply one, and the response tells you what it generated:
db.animals.insertOne({ name: 'Roger', species: 'dog', favoriteFoods: ['biscuits'] })
// { acknowledged: true, insertedId: ObjectId('688f4a2e9d1c2a7b3e4f5a60') }
db.animals.insertMany([
{ name: 'Buck', species: 'dog', favoriteFoods: ['carrots'] },
{ name: 'Luna', species: 'cat', favoriteFoods: ['tuna', 'chicken'] },
{ name: 'Togo', species: 'dog', favoriteFoods: ['biscuits', 'carrots'] }
])
// { acknowledged: true, insertedIds: { '0': ObjectId('...'), '1': ObjectId('...'), '2': ObjectId('...') } }
The collection is created on the first insert. The old insert() method still exists in some drivers but is not the right API for new code.
The Node.js driver uses the same insertMany() method on a collection:
const animals = (await getDb()).collection('animals')
const result = await animals.insertMany([
{ name: 'Buck', species: 'dog' },
{ name: 'Luna', species: 'cat' }
])
console.log(result.insertedCount) //2
Await the result so you can handle write errors and inspect how many documents were inserted.
By default, insertMany() is ordered. MongoDB stops at the first document that fails, for example because it violates a unique index. Documents inserted before the failure remain in the collection, while later documents are skipped.
Pass { ordered: false } when the documents are independent and MongoDB should attempt the rest of the batch:
await animals.insertMany(documents, { ordered: false })
An unordered batch can still report a bulk write error, so handle the error and inspect its result instead of assuming the whole array was inserted.
Reading with filters and projections
Read with find() and pass a filter document. An empty filter matches everything:
db.animals.find({ species: 'dog' })
db.animals.findOne({ name: 'Roger' })
A filter is a document, not a SQL expression. Equality is { species: 'dog' }, comparisons use operators like { age: { $gte: 5 } }. If findOne() matches several documents it returns the first one according to the query plan, so add a unique field to the filter when the exact record matters.
A projection limits the fields that come back — the second argument to find():
db.animals.find({ name: 'Roger' }, { name: 1, favoriteFoods: 1, _id: 0 })
// [ { name: 'Roger', favoriteFoods: [ 'biscuits' ] } ]
Returning less data matters once documents grow beyond toy size.
The ObjectId trap
Here is the mistake everyone makes once. You copy an _id from earlier output and query with it as a string:
db.animals.find({ _id: '688f4a2e9d1c2a7b3e4f5a60' })
// (no results)
db.animals.find({ _id: ObjectId('688f4a2e9d1c2a7b3e4f5a60') })
// [ { _id: ObjectId('688f4a2e9d1c2a7b3e4f5a60'), name: 'Roger', ... } ]
Nothing errors. The string form of an ObjectId simply does not equal the ObjectId value, so the first query matches zero documents. In application code this usually appears as “the document exists in the shell but my API returns 404”: the route handler received the ID as a string and never converted it. Wrap it in ObjectId() (or new ObjectId(id) in the Node driver) before filtering.
Lesson completed