Databases · 01/09 8 min
Mongoose & MongoDB essentials
Queries, ObjectIds, CastError, {new: true}, populate, unique indexes, hooks.
Queries you'll see every time
| Call | Returns | Gotcha |
|---|---|---|
Model.find(filter) |
array (maybe empty) | never null, so if (!docs) is wrong |
Model.findOne(filter) |
doc or null |
check for null → 404 |
Model.findById(id) |
doc or null |
invalid id string → CastError |
findByIdAndUpdate(id, upd) |
the old doc | pass { new: true } for the updated one |
Model.create(data) |
new doc | runs validators and pre('save') hooks |
updateOne / updateMany |
{ matchedCount, modifiedCount } |
skips save hooks |
ObjectIds
doc._id is an ObjectId object, not a string. doc.owner === req.user.id is always false; use doc.owner.equals(id) or String(doc.owner) === id.
Errors to translate (not 500!)
- CastError (bad id) →
400 - ValidationError (required/min/enum) →
400with the field messages - E11000 duplicate key (unique index) →
409 Conflict nullresult →404
Hooks and this
// ❌ arrow function: `this` is not the document
userSchema.pre("save", async () => { this.password = await hash(this.password); });
// ✅ and only re-hash when it changed
userSchema.pre("save", async function () {
if (this.isModified("password")) this.password = await bcrypt.hash(this.password, 10);
});
Atomic updates
Read-modify-write in JS (doc.stock -= 1; await doc.save()) races under concurrency. Push the condition into the query:
await Product.updateOne({ _id, stock: { $gte: qty } }, { $inc: { stock: -qty } });
populate
Order.find().populate("user", "name email") swaps the stored ObjectId for the referenced document. Without it, order.user.name is undefined.
Practise it now
Next in Databases
Your first database with Node