Queries & CRUD
Mondel collection methods mirror the driver while adding validation, timestamps, and typed schema access.
const db = await connect(process.env.MONGODB_URI!);Create
// Field defaults (e.g. role: "USER") apply when validation is strict/loose
const result = await db.users.create({
email: "john@example.com",
name: "John",
});
// result is InsertOneResult — use result.insertedId (not a full document)
await db.users.createMany([
{ email: "a@test.com" },
{ email: "b@test.com" },
]);Options: session, timestamps (default follows schema), plus native insert options.
Unknown keys are preserved (not stripped) on validated creates.
Read
const user = await db.users.findOne({ email: "john@example.com" });
const users = await db.users.findMany(
{ role: "ADMIN" },
{
sort: { createdAt: -1 },
skip: 0,
limit: 20,
select: { email: true, role: true, _id: true }, // runtime projection
}
);
const byId = await db.users.findById("507f1f77bcf86cd799439011");Native operators:
await db.users.findMany({
age: { $gte: 18 },
role: { $in: ["ADMIN", "MODERATOR"] },
});Projection vs TypeScript
select maps to MongoDB projection at runtime.
The TypeScript return type remains the full document type today — it is not narrowed by select. Access only fields you projected.
Driver options such as session, timeoutMS, and maxTimeMS are forwarded on find helpers.
Update
// Plain object → wrapped in $set
await db.users.updateOne({ email: "john@example.com" }, { name: "Johnny" });
// Operators
await db.users.updateOne(
{ _id: userId },
{ $set: { name: "Johnny" }, $inc: { loginCount: 1 } }
);
await db.users.updateById(userId, { $set: { name: "Johnny" } });
// Atomic find + update
await db.users.findOneAndUpdate(
{ email: "john@example.com" },
{ $set: { lastSeenAt: new Date() } },
{ returnDocument: "after" }
);Validation: plain objects and $set / $setOnInsert payloads are validated.$inc, $push, $pull, $unset, etc. are not schema-validated.
Partial updates do not inject field defaults (only create does).
Timestamps on update
With timestamps: true on the schema:
- Updates set
updatedAt - Upserts also set
createdAtvia$setOnInsert - Disable with
{ timestamps: false }
Delete
// Hard delete (default)
await db.users.deleteOne({ email: "gone@example.com" });
await db.users.deleteMany({ isActive: false });
await db.users.deleteById(userId);
// Soft delete — sets deletedAt (and updatedAt when timestamps enabled)
await db.users.deleteOne({ email: "gone@example.com" }, { soft: true });See Soft delete recipe.
Utilities
const n = await db.users.count({ role: "ADMIN" });
const ok = await db.users.exists({ email: "john@example.com" });Aggregation
const stats = await db.users.aggregate([
{ $match: { isActive: true } },
{ $group: { _id: "$role", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
]);Bulk write
await db.users.bulkWrite([
{ insertOne: { document: { email: "a@test.com" } } },
{
updateOne: {
filter: { email: "b@test.com" },
update: { $set: { active: true } },
},
},
]);Payloads are not re-validated. Prefer create / updateOne when you need Zod.
Raw collection
const col = db.users.getCollection();
await col.distinct("role");Transactions
Pass { session } into CRUD methods. Full guide: Transactions.
