MongoDB interview revision notes
NoSQL document database: CRUD, Mongoose, aggregation. Written for developers preparing for an interview who want to refresh their MongoDB fast. Free, no sign-up.
20 topics in these notes
- What is MongoDB?
- Document vs relational
- Databases & collections
- CRUD – Insert
- CRUD – Find & query operators
- Projection, limit, skip
- CRUD – Update
- CRUD – Delete
- Indexes
- Mongoose intro (Schema, Model)
- Mongoose CRUD
- Mongoose validation
- Relationships (ref, populate)
- Aggregation pipeline
- Connection & connection string
- Transactions
- Performance (indexes, lean)
- Security & auth
- Mongoose middleware (pre/post)
- Mongoose virtuals & methods/statics
What is MongoDB?
Simple real-life example (Understand first — very important for understanding)
- Think of a filing cabinet where each drawer can hold different-shaped folders—not every folder has the same fields.
- MongoDB stores data as flexible documents (often like JSON): each record can have the fields it needs, and you can query and update them.
- Good when your data doesn't fit neatly into fixed tables or when structure evolves over time.
Real-world idea: store data in flexible documents instead of rigid tables. MongoDB is built for that style.
Technical definition
- MongoDB is a NoSQL document database. Data is stored as BSON documents (JSON-like) in collections.
- Schema is flexible: documents in the same collection can have different fields. No fixed tables/columns like SQL.
- Used for: content, catalogs, real-time apps, when schema evolves or varies. Can be self-hosted or MongoDB Atlas (cloud).
Simple practical example
Schema is flexible: documents in the same collection can have different fields.
Code example
// A document in a "users" collection
{ _id: ObjectId("..."), name: "Alice", age: 30, tags: ["admin"] }Why it matters
Full-stack and backend roles often use MongoDB with Node. "What is MongoDB?" and "When would you use it vs SQL?" are common.
Interview tip
"MongoDB is a document (NoSQL) database; data as BSON in collections. Flexible schema; good when structure varies or evolves."
Document vs relational
Simple real-life example (Understand first — very important for understanding)
- Relational is like spreadsheets: fixed columns, rows, and links between tables (e.g. customers and orders).
- Document is like folders: each document can have its own shape; you can nest related data in one place or link by ID.
- Choose document when structure varies or you want to read whole "things" in one go; choose relational when you need strict relations and joins.
Real-world idea: two ways to organize data. Documents are flexible and nested; tables are strict and linked.
Technical definition
- Relational (SQL): tables, rows, columns; fixed schema; JOINs; ACID; good for heavy relations and strict consistency.
- Document (MongoDB): collections, documents (nested objects/arrays); flexible schema; embed or reference; scale-out (sharding).
- MongoDB: embed when one-to-few and data is accessed together; reference (store ID) when one-to-many or shared. No JOINs; use $lookup (aggregation) or multiple queries.
Simple practical example
Document (MongoDB): collections, documents (nested objects/arrays); flexible schema; embed or reference; scale-out (sharding).
Code example
// Embed: address inside user
{ _id: 1, name: "Alice", address: { city: "NY", zip: "10001" } }
// Reference: order has userId
{ _id: 1, userId: 2, total: 99 }Why it matters
Interview: "MongoDB vs SQL?" — schema flexibility, embedding vs normalization, when to use which.
Interview tip
"Document DB: flexible schema, embed or reference. Use embed for one-to-few; reference for many or shared. No JOINs; $lookup or app-level."
Databases & collections
Simple real-life example (Understand first — very important for understanding)
- A database is like one building; collections are like floors or departments—each holds many documents of a similar kind.
- You don't need to "create" them first in the same way as tables; you use a database and collection by name, and they appear when you add data.
- Naming is flexible but keep it clear (e.g. users, orders, products).
Real-world idea: database = building, collection = department. You put documents in a collection and query by database and collection name.
Technical definition
- MongoDB has databases; each has collections (like tables); collections have documents (like rows but JSON).
- Create by use:
use mydb(shell); first insert creates collection.db.collectionNamereferences a collection. - Naming: databases and collections; avoid reserved names. No need to pre-declare schema.
Simple practical example
Create by use: use mydb (shell); first insert creates collection.
Code example
// mongosh
use myapp
db.users.insertOne({ name: "Bob" }) // creates db myapp and collection usersWhy it matters
Hierarchy: server → database → collection → document. You reference db and collection in every operation.
Interview tip
"Database contains collections; collections contain documents. Created on first write; no schema declaration."
CRUD – Insert
Simple real-life example (Understand first — very important for understanding)
- Like adding a new card to a Rolodex: you write down the details and put it in; the system gives it an ID so you can find it later.
- Insert adds one or many documents; you can let the server generate an _id or provide your own.
- Once inserted, the document is stored and you can read or update it by that _id.
Real-world idea: add new records. Insert is how you create documents; the rest of CRUD is read, update, and delete.
Technical definition
insertOne(doc)— insert one document; returns{ acknowledged, insertedId }. MongoDB adds_idif missing (ObjectId).insertMany([doc1, doc2])— insert many; returnsinsertedIds. Order by default;ordered: falsecontinues on duplicate key error.- Document can be nested objects and arrays. Dates as
new Date(); store in UTC.
Simple practical example
insertMany([doc1, doc2]) — insert many; returns insertedIds.
Code example
const result = await db.collection('users').insertOne({ name: 'Alice', age: 30 });
console.log(result.insertedId);
await db.collection('users').insertMany([
{ name: 'Bob' },
{ name: 'Carol' }
]);Why it matters
Create operations are the first CRUD step. insertedId is used for follow-up reads or references.
Interview tip
"insertOne returns insertedId; insertMany returns insertedIds. _id auto-generated if omitted (ObjectId)."
CRUD – Find & query operators
Simple real-life example (Understand first — very important for understanding)
- Like searching the filing cabinet: "Give me all folders where status is 'active'," or "Give me the one with this ID."
- Find takes a filter (e.g. { status: 'active' }) and returns matching documents; you can limit how many and in what order.
- You can match on any field, nest conditions, and use operators for "greater than," "in list," "exists," etc.
Real-world idea: read by criteria. Find is your main way to query documents; filters and options shape the result.
Technical definition
find(filter)— returns cursor (many);findOne(filter)— returns one document or null. Filter is a query object.- Query operators:
$eq,$ne,$gt,$gte,$lt,$lte,$in,$nin,$and,$or,$exists,$regex. - Example:
{ age: { $gte: 18 } },{ status: { $in: ['active', 'pending'] } },{ $or: [{ a: 1 }, { b: 2 }] }.
Simple practical example
Query operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $and, $or, $exists, $regex.
Code example
await db.collection('users').findOne({ _id: id });
await db.collection('users').find({ age: { $gte: 18 }, status: 'active' }).toArray();Why it matters
Read is the most common operation. Filter syntax and operators are asked in interviews.
Interview tip
"find returns cursor; findOne returns doc or null. Use $eq, $gt, $in, $or, $exists, $regex in filter object."
Projection, limit, skip
Simple real-life example (Understand first — very important for understanding)
- Projection is like asking for only certain columns: "Just name and email, not the whole record." You get smaller, faster results.
- Limit is like "top 10" or "first 5": you cap how many documents come back so you don't overload the client or server.
- Sort orders the results (e.g. newest first, or by name); skip lets you "page" (e.g. skip 20, limit 10 for page 3).
Real-world idea: choose fields and how many. Projection and limit/skip keep queries efficient and support pagination.
Technical definition
- Projection: second arg to find —
find({}, { name: 1, email: 1, _id: 0 }). 1 = include, 0 = exclude. Reduce data transferred. .limit(n)— max documents;.skip(n)— skip first n. Used together for pagination:skip((page-1)*size).limit(size).- Skip is inefficient on large offsets (server still walks docs). Prefer range queries (e.g. _id > lastSeen) for cursor-based pagination.
Simple practical example
.
Code example
find({}, { name: 1, _id: 0 }).limit(10).skip(20)
// Page 3, 10 per pageWhy it matters
Projection saves bandwidth; limit/skip are basic pagination. Interview: "How do you paginate?" — limit/skip or cursor-based.
Interview tip
"Projection: second arg 1/0. limit/skip for pagination; mention skip is slow at large offset; cursor-based is better for big data."
CRUD – Update
Simple real-life example (Understand first — very important for understanding)
- Like correcting a form: change one field, or several, or replace the whole document, and optionally only if a condition is still true.
- Update uses a filter (which document(s)) and an update (what to set, increment, push to array, etc.).
- Upsert means "update if exists, otherwise insert"—one call to "ensure this exists with this data."
Real-world idea: change existing documents. Update and upsert let you fix or create records in one step.
Technical definition
updateOne(filter, update)— update first match;updateMany(filter, update)— update all matches. Use$setto set fields:{ $set: { name: 'New' } }.- Without
$set, the document can be replaced (depending on driver). Always use update operators:$set,$unset,$inc,$push,$pull. findOneAndUpdate(filter, update, { returnDocument: 'after' })— update and return updated doc.upsert: truecreates if no match.
Simple practical example
Without $set, the document can be replaced (depending on driver).
Code example
await db.collection('users').updateOne(
{ _id: id },
{ $set: { name: 'Alice', updatedAt: new Date() } }
);
await db.collection('counters').updateOne(
{ _id: 'views' },
{ $inc: { count: 1 } },
{ upsert: true }
);Why it matters
Update is critical for state changes. $set vs replace and update operators are frequently asked.
Interview tip
"updateOne/updateMany with $set, $unset, $inc, $push, $pull. Use $set to change fields; upsert: true to create if not found."
CRUD – Delete
Simple real-life example (Understand first — very important for understanding)
- Like removing a card from the Rolodex: you say which one(s) to remove (by filter or _id), and they are gone.
- Delete can remove one or many; always use a filter so you don't wipe the whole collection by mistake.
- After delete, those documents are no longer returned by find.
Real-world idea: remove records. Delete is the last part of CRUD; use filters carefully so you only remove what you intend.
Technical definition
deleteOne(filter)— delete first match;deleteMany(filter)— delete all matches. Returns{ deletedCount }.- Filter can be
{ _id: id }for one doc. Empty filter{}in deleteMany deletes all in collection (dangerous). - Soft delete: instead of removing,
$set: { deleted: true, deletedAt: new Date() }and filter out in queries.
Simple practical example
Filter can be { _id: id } for one doc.
Code example
await db.collection('users').deleteOne({ _id: id });
await db.collection('logs').deleteMany({ createdAt: { $lt: cutoff } });Why it matters
Delete is straightforward; know the difference between deleteOne and deleteMany and when to soft delete.
Interview tip
"deleteOne/deleteMany; returns deletedCount. Soft delete: set deleted flag and filter in reads."
Indexes
Simple real-life example (Understand first — very important for understanding)
- Like an index at the back of a book: instead of reading every page to find a topic, you look up the page number and jump there.
- Indexes make queries on certain fields (or combinations) fast; without them, the database may scan many documents.
- Create indexes for fields you filter or sort on often; balance speed of reads with the cost of maintaining the index.
Real-world idea: speed up lookups. Indexes turn "search everything" into "jump to the right place."
Technical definition
- Indexes speed up queries (find, sort) and enforce uniqueness. Without index on filter/sort fields, collection scan (slow on large data).
createIndex({ field: 1 })— ascending;{ field: -1 }descending.createIndex({ name: 1 }, { unique: true })for unique.- Compound index:
{ userId: 1, createdAt: -1 }— good for queries on userId and sort by createdAt. Order of keys matters for query patterns. - _id has a unique index by default. Explain:
find(...).explain()to see if index used.
Simple practical example
createIndex({ field: 1 }) — ascending; { field: -1 } descending.
Code example
await db.collection('users').createIndex({ email: 1 }, { unique: true });
await db.collection('orders').createIndex({ userId: 1, createdAt: -1 });Why it matters
Production queries need indexes. "Why is my query slow?" — often missing index. Unique index for uniqueness.
Interview tip
"Indexes speed find/sort; createIndex. Unique index for uniqueness. Compound index for multi-field queries; key order matters."
Mongoose intro (Schema, Model)
Simple real-life example (Understand first — very important for understanding)
- Like a form template: you define what fields a document can have, their types, and optional validation.
- Mongoose sits on top of MongoDB and gives you schemas, validation, and methods so you work with JavaScript objects instead of raw documents.
- Same idea as "define the shape of your data once" so the app stays consistent and you catch errors early.
Real-world idea: structure and validate in code. Mongoose adds schemas and helpers on top of MongoDB.
Technical definition
- Mongoose is an ODM (Object-Document Mapper) for MongoDB in Node. Provides schema, validation, middleware, population.
- Schema: define shape and types —
new mongoose.Schema({ name: String, age: Number, createdAt: { type: Date, default: Date.now } }). - Model:
mongoose.model('User', userSchema)— compiles schema into a model; use Model to query (e.g.User.find()). Collection name is pluralized (users).
Simple practical example
Schema: define shape and types — new mongoose.
Code example
const schema = new mongoose.Schema({ name: String, age: Number });
const User = mongoose.model('User', schema);
// User is the model; operations: User.find(), User.create(), etc.Why it matters
Most Node + MongoDB projects use Mongoose. Schema and Model are the first concepts.
Interview tip
"Mongoose: Schema defines shape, Model is compiled Schema. Model name singular, collection plural. Use Model for CRUD."
Mongoose CRUD
Simple real-life example (Understand first — very important for understanding)
- Create: build a new document from the model, fill fields, call save (or use create).
- Read: find, findOne, findById with filters and options; you get Mongoose documents with methods and virtuals.
- Update/Delete: find then save, or use updateOne/deleteOne; Mongoose can validate and run hooks.
Real-world idea: same CRUD, with schemas and methods. Mongoose CRUD keeps your data shaped and validated.
Technical definition
User.create(doc)ornew User(doc).save()— insert.User.find(),User.findOne(),User.findById(id)— read.User.updateOne(),User.updateMany(),User.findByIdAndUpdate(id, update, { new: true })— update;new: truereturns updated doc.User.deleteOne(),User.deleteMany(),User.findByIdAndDelete(id). All return promises; use async/await.
Simple practical example
User.
Code example
const user = await User.create({ name: 'Alice', age: 30 });
const u = await User.findByIdAndUpdate(id, { name: 'Bob' }, { new: true });Why it matters
Day-to-day CRUD in Node is done through Mongoose. Know create, find/findOne/findById, update, delete.
Interview tip
"create/save for insert; find, findOne, findById for read; updateOne, findByIdAndUpdate (new: true); deleteOne, findByIdAndDelete."
Mongoose validation
Simple real-life example (Understand first — very important for understanding)
- Like a form that won't submit until required fields are filled and email looks like an email.
- You define rules in the schema (required, type, min/max, enum, custom); Mongoose runs them on save/create.
- Invalid data is rejected with clear errors so bad documents never get into the database.
Real-world idea: enforce rules before saving. Validation in Mongoose keeps your data clean at the app layer.
Technical definition
- Schema types and options:
required: true,default,min/max(numbers),enum,minlength/maxlength(strings). - Custom validator:
validate: { validator: (v) => ... , message: '...' }. Runs on save/create; invalid doc throws ValidationError. - Casting: Mongoose casts types (e.g. string to number if schema says Number). Invalid cast can throw. Handle in try/catch or error middleware.
Simple practical example
Custom validator: validate: { validator: (v) => .
Code example
name: { type: String, required: true, minlength: 2 },
age: { type: Number, min: 0, max: 150 },
role: { type: String, enum: ['user', 'admin'] }Why it matters
Validation at the model layer keeps invalid data out. required, enum, and custom validators are common.
Interview tip
"Schema: required, default, min/max, enum, minlength/maxlength; custom validate. ValidationError on save if invalid."
Relationships (ref, populate)
Simple real-life example (Understand first — very important for understanding)
- Like "this order belongs to this customer" or "this post has many comments." You can store a reference (ID) or embed the related data.
- Reference: store only the id; when you need the full object, you fetch it (or populate). Embed: put the nested data inside the document.
- Choose reference when the related thing is shared or large; embed when it's always read and updated with the parent.
Real-world idea: link or nest data. References and embeds are the two main ways to model relationships in documents.
Technical definition
- Store reference:
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }. ref tells Mongoose which model to populate. Model.find().populate('userId')— replace userId with the actual User document(s).populate('path', 'name email')to select fields.- One-to-many: many side has ref to one (e.g. posts have authorId ref User). Populate on the many side. Virtual for "reverse" (user's posts) if needed.
Simple practical example
Model.
Code example
const post = await Post.findById(id).populate('authorId', 'name');
// post.authorId is now full user doc (name only)Why it matters
Related data is stored by ID; populate loads it. "How do you do relations in Mongoose?" — ref + populate.
Interview tip
"Store ObjectId with ref: 'Model'. populate('field') to replace ID with document. Select fields as second arg to populate."
Aggregation pipeline
Simple real-life example (Understand first — very important for understanding)
- Like a pipeline: first filter, then group by category, then sum or average, then sort—each step passes results to the next.
- Aggregation does complex analytics (counts, sums, averages, grouping) inside the database so you don't pull all data into the app.
- Use it for reports, dashboards, or any "group by" or multi-step transformation.
Real-world idea: multi-step analysis in the database. Aggregation pipelines filter, group, and compute in one place.
Technical definition
- Aggregation: stages in a pipeline; each stage transforms documents.
aggregate([ { $match: { ... } }, { $group: { _id: '$status', count: { $sum: 1 } } } ]). - Stages:
$match(filter),$group(group by, $sum, $avg, $push),$sort,$project(reshape),$lookup(join),$unwind(flatten array). - Result is cursor or array. Use for analytics, counts by group, complex reporting. More powerful than single find; can be heavy — index $match stages.
Simple practical example
Stages: $match (filter), $group (group by, $sum, $avg, $push), $sort, $project (reshape), $lookup (join), $unwind (flatten array).
Code example
await db.collection('orders').aggregate([
{ $match: { status: 'completed' } },
{ $group: { _id: '$userId', total: { $sum: '$amount' } } },
{ $sort: { total: -1 } }
]).toArray();Why it matters
Complex queries (group by, sum, join-like) use aggregation. $match, $group, $lookup are frequently asked.
Interview tip
"Pipeline: $match (filter), $group (sum, avg, push), $sort, $project, $lookup (join), $unwind. Index $match for performance."
Connection & connection string
Simple real-life example (Understand first — very important for understanding)
- Like plugging into the power grid: you give the address (connection string), maybe options (timeout, pool size), and the driver connects.
- You usually connect once at startup and reuse the connection; the driver handles pooling and reconnects.
- Connection string contains host, database name, and often auth; keep it in env, not in code.
Real-world idea: connect once, use everywhere. Connection and pooling let your app talk to MongoDB efficiently.
Technical definition
- Connect:
mongoose.connect(uri)orMongoClient.connect(uri). URI:mongodb://host:27017/dbnameormongodb+srv://user:pass@cluster.mongodb.net/dbname(Atlas). - Options:
{ useNewUrlParser: true, useUnifiedTopology: true }(often default). Connection is async; wait before using models:await mongoose.connect(uri)at startup. - Never put credentials in code; use
process.env.MONGODB_URI. Atlas uses SRV and SSL by default.
Simple practical example
Options: { useNewUrlParser: true, useUnifiedTopology: true } (often default).
Code example
await mongoose.connect(process.env.MONGODB_URI);
// or
const client = await MongoClient.connect(process.env.MONGODB_URI);
const db = client.db('myapp');Why it matters
Every app connects with a URI. Local vs Atlas and env-based config are standard.
Interview tip
"mongoose.connect(uri) or MongoClient.connect(uri). Use env for URI. Atlas: mongodb+srv://... with SSL."
Transactions
Simple real-life example (Understand first — very important for understanding)
- Like a single transaction at a bank: either all steps succeed (transfer from A to B, update both balances) or none do.
- Transactions group several operations so they commit together or roll back together—no half-updated state.
- Use when one logical action touches multiple documents or collections and must stay consistent.
Real-world idea: all or nothing. Transactions keep multi-step updates consistent.
Technical definition
- Transactions: multi-document ACID. Start session, start transaction, do operations, commit or abort. Requires replica set (not standalone).
- Mongoose:
const session = await mongoose.startSession(); session.startTransaction();then pass{ session }to operations;await session.commitTransaction()orabortTransaction(). - Use when: transfer between accounts, order + inventory update together. Overhead; use only when consistency across docs is required.
Simple practical example
Mongoose: const session = await mongoose.
Code example
const session = await mongoose.startSession();
session.startTransaction();
try {
await Model1.updateOne(..., { session });
await Model2.updateOne(..., { session });
await session.commitTransaction();
} catch (e) {
await session.abortTransaction();
} finally {
session.endSession();
}Why it matters
When you need all-or-nothing across multiple writes, transactions are the answer. Interview: "How do you do multi-doc consistency?"
Interview tip
"Transactions for multi-doc ACID; need replica set. startSession, startTransaction, pass session to ops, commit or abort."
Performance (indexes, lean)
Simple real-life example (Understand first — very important for understanding)
- Like organizing a warehouse: indexes help you find things fast; avoid loading huge lists when you only need a page; cache when reads are repeated.
- Use projection to fetch only needed fields; use limit/skip or cursor for large result sets; watch for N+1 (many small queries instead of one).
- Monitor slow queries and add indexes or change queries based on real usage.
Real-world idea: indexes, lean queries, and caching. Performance is about doing less work and finding data faster.
Technical definition
- Indexes: ensure filter and sort fields are indexed; use explain() to verify. Compound index order should match query pattern.
- Projection: select only needed fields (Mongoose:
.select('name email')) to reduce data and memory. - Mongoose
.lean()— returns plain objects instead of Mongoose documents; faster and less memory for read-only use. No save/getters/setters. - Avoid N+1: use populate in one query or aggregation $lookup instead of looping and find per item.
Simple practical example
Projection: select only needed fields (Mongoose: .
Code example
await User.find().select('name email').lean(); // fast read-only
await Order.find({ userId }).explain(); // check index usageWhy it matters
Slow APIs often need indexes or lean(). "How do you optimize Mongoose queries?" — indexes, projection, lean, avoid N+1.
Interview tip
"Index filter/sort fields; use explain. select() for projection; lean() for read-only plain objects. Avoid N+1 with populate or $lookup."
Security & auth
Simple real-life example (Understand first — very important for understanding)
- Like locking the filing cabinet: restrict who can connect (auth), who can read/write which collections (roles), and never trust client input in queries.
- Use strong auth (e.g. SCRAM); avoid exposing the server to the whole internet; validate and sanitize so injection can't happen.
- Keep the driver and server updated; use TLS for connections in production.
Real-world idea: auth, roles, and safe input. Security in MongoDB is connection auth, access control, and safe queries.
Technical definition
- Authentication: store hashed passwords (bcrypt), never plain text. Compare with
bcrypt.compare()on login. Use JWT or sessions for auth state. - Authorization: check ownership or role before update/delete (e.g. req.user.id === resource.userId or req.user.role === 'admin').
- DB: enable auth (MongoDB auth or Atlas); use strong credentials; network isolation; avoid exposing DB port. Validate/sanitize input to prevent injection (MongoDB uses typed queries; still validate).
Simple practical example
Authorization: check ownership or role before update/delete (e.
Code example
const hash = await bcrypt.hash(password, 10);
const ok = await bcrypt.compare(plain, user.passwordHash);
// In route: if (req.user.id !== resource.userId) return res.status(403).json(...);Why it matters
Auth and authorization are required in real apps. "How do you store passwords?" — hash only; "How do you protect routes?" — middleware + ownership/role check.
Interview tip
"Passwords: hash (bcrypt), never store plain. Auth: JWT or session. Authorize: check ownership/role before modify. DB: auth enabled, env for credentials."
Mongoose middleware (pre/post)
Simple real-life example (Understand first — very important for understanding)
- Like a checkpoint before and after an action: "Before save, set the updated date"; "After delete, clean up related cache."
- Middleware (pre/post hooks) runs at defined events (save, remove, etc.); you can run async code and modify the document or do side effects.
- Use for timestamps, hashing passwords, or invalidating caches—shared logic in one place.
Real-world idea: run code at key moments. Mongoose middleware keeps cross-cutting logic in one place.
Technical definition
- Middleware (hooks) run before or after certain operations:
schema.pre('save', function(next) { ...; next(); }),schema.post('find', function(docs) { }). - Common:
pre('save')— hash password, set timestamps;post('remove')— delete related docs;pre('findOneAndDelete')— cascade. Usethisfor document (in pre/post save). - Async:
schema.pre('save', async function() { await ... }); no need to call next when using async. Can throw to abort.
Simple practical example
Common: pre('save') — hash password, set timestamps; post('remove') — delete related docs; pre('findOneAndDelete') — cascade.
Code example
userSchema.pre('save', async function() {
if (this.isModified('password'))
this.password = await bcrypt.hash(this.password, 10);
});
userSchema.pre('remove', async function() {
await Post.deleteMany({ authorId: this._id });
});Why it matters
Password hashing, timestamps, and cascade deletes are often done in hooks. "Where do you hash the password?" — pre('save') middleware.
Interview tip
"pre/post hooks: pre('save') for hash password, timestamps; post('remove') for cascade. this = document in pre save; async ok, throw to abort."
Mongoose virtuals & methods/statics
Simple real-life example (Understand first — very important for understanding)
- Virtual: a field that isn't stored but computed when you read it (e.g. fullName from firstName + lastName).
- Method: a function on the document (e.g. user.checkPassword()) so behavior lives with the data.
- Both keep the schema clear and put derived data and behavior where they belong.
Real-world idea: computed fields and document behavior. Virtuals and methods make documents smarter without duplicating data.
Technical definition
- Virtuals: computed properties not stored in DB.
schema.virtual('fullName').get(function() { return this.first + ' ' + this.last }). Use in JSON iftoJSON: { virtuals: true }. - Instance methods:
schema.methods.comparePassword = function(candidate) { return bcrypt.compare(candidate, this.password) }. Call on document:user.comparePassword(plain). - Statics:
schema.statics.findByEmail = function(email) { return this.findOne({ email }) }. Call on Model:User.findByEmail('a@b.com'). Use for reusable query helpers.
Simple practical example
Instance methods: schema.
Code example
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
userSchema.methods.comparePassword = function(pw) {
return bcrypt.compare(pw, this.passwordHash);
};
userSchema.statics.findActive = function() {
return this.find({ status: 'active' });
};Why it matters
Virtuals for derived fields; instance methods for document behavior (e.g. comparePassword); statics for model-level helpers. Often asked in Mongoose interviews.
Interview tip
"Virtual: computed, not in DB; schema.virtual().get(). Instance methods: on document (user.method()). Statics: on Model (User.static())."
Practise the questions
Read the top 50 MongoDB interview questions and answers to test yourself after revising.