Mongoose ref and populate: A Deep Dive
ref and populate are how MongoDB handles relationships. Here is a deep dive.
Mongoose ref and populate: A Deep Dive
MongoDB is NoSQL, but real apps have relationships. Mongoose handles them with ref and populate. Here is a deep dive.
The ref Field
const messageSchema = new mongoose.Schema({
senderId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
connectionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Connection', required: true },
text: { type: String, required: true }
});
The field stores an ObjectId. The ref tells Mongoose which model to use when populating.
Basic populate
const messages = await Message.find()
.populate('senderId', 'firstName lastName photoUrl');
Returns messages with senderId replaced by the user object (only firstName, lastName, photoUrl).
Populate Multiple Fields
const connection = await Connection.findById(id)
.populate('user1Id', 'firstName photoUrl')
.populate('user2Id', 'firstName photoUrl');
Nested populate
const posts = await Post.find()
.populate({
path: 'comments',
populate: { path: 'authorId', select: 'firstName' }
});
Each level of populate does an extra query. Nested populate can be expensive; use sparingly.
Populate With Filter
const user = await User.findById(id)
.populate({
path: 'posts',
match: { status: 'published' },
options: { sort: { createdAt: -1 }, limit: 10 }
});
Filters and sorts the populated documents.
Populate With Virtuals
For relationships that are not stored as a field, use a virtual populate:
userSchema.virtual('posts', {
ref: 'Post',
localField: '_id',
foreignField: 'authorId'
});
const user = await User.findById(id).populate('posts');
The virtual looks up posts where authorId matches the user's _id.
The N+1 Problem
If you loop over messages and call User.findById for each sender, you do N+1 queries. Use populate to do it in 2 queries.
populate Has a Cost
populate does an extra query (or more for nested). For high-traffic endpoints, denormalize: store the sender's name on the message so you do not need to populate. Trade storage for query speed.
Select Specific Fields
Always populate with a select. Do not return the entire referenced document (especially not the password hash):
.populate('senderId', 'firstName lastName photoUrl')
When to Use ref vs embed
- ref: for one-to-many with large N (messages, connections, payments). Keeps documents small. Allows pagination.
- embed: for small, tightly coupled data (an address inside a user). Loads in one query.
The Takeaway
Use ref to reference another collection and populate to join on read. Populate multiple fields, nest with care, filter and sort populated docs, use virtuals for non-stored relationships. Avoid N+1 with populate. Always select specific fields. Denormalize for high-traffic endpoints. Use ref for large N, embed for small data.
You define a ref field that stores an ObjectId and references another model. On read, .populate('senderId', 'firstName') does an extra query to fetch the referenced document and replaces the ObjectId with the document. Always select specific fields.
Populating a field, then populating a field inside that. Example: .populate({ path: 'comments', populate: { path: 'authorId', select: 'firstName' } }). Each level does an extra query; use sparingly.
For relationships not stored as a field. userSchema.virtual('posts', { ref: 'Post', localField: '_id', foreignField: 'authorId' }). Then User.findById(id).populate('posts') looks up posts where authorId matches the user's _id.
Looping over documents and calling findById for each reference does N+1 queries. Use populate to do it in 2 queries (one for the parent, one for all referenced docs). Much faster.
Use ref for one-to-many with large N (messages, connections, payments). Keeps documents small and allows pagination. Use embed for small, tightly coupled data (an address inside a user) that always loads together.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

