RSRobince Studio

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

  1. What is MongoDB?
  2. Document vs relational
  3. Databases & collections
  4. CRUD – Insert
  5. CRUD – Find & query operators
  6. Projection, limit, skip
  7. CRUD – Update
  8. CRUD – Delete
  9. Indexes
  10. Mongoose intro (Schema, Model)
  11. Mongoose CRUD
  12. Mongoose validation
  13. Relationships (ref, populate)
  14. Aggregation pipeline
  15. Connection & connection string
  16. Transactions
  17. Performance (indexes, lean)
  18. Security & auth
  19. Mongoose middleware (pre/post)
  20. 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.collectionName references 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 users

Why 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 _id if missing (ObjectId).
  • insertMany([doc1, doc2]) — insert many; returns insertedIds. Order by default; ordered: false continues 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 page

Why 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 $set to 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: true creates 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) or new User(doc).save() — insert. User.find(), User.findOne(), User.findById(id) — read.
  • User.updateOne(), User.updateMany(), User.findByIdAndUpdate(id, update, { new: true }) — update; new: true returns 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) or MongoClient.connect(uri). URI: mongodb://host:27017/dbname or mongodb+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() or abortTransaction().
  • 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 usage

Why 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. Use this for 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 if toJSON: { 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())."

More Databases notes

Mongoose · DBMS and SQL · All developer notes