Referencing and populate
Objectives
By the end of this lesson, you should be able to:
- Reference another document by its
ObjectIdinstead of embedding it - Load a referenced document with
populate - Explain when referencing fits better than embedding
💡 Why this matters: Last lesson’s comments belonged exclusively to one post, and embedding fit well. An author who writes many posts is a different shape, referencing, MongoDB’s version of a foreign key, is the better fit here.
⚠️ A note on verification: this tooling has no route to a local MongoDB server. The code below reflects Mongoose’s stable, current, documented API. Try it yourself, on your own machine, against a real MongoDB server.
Defining a Reference
const authorSchema = new mongoose.Schema({
name: { type: String, required: true }
});
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
body: { type: String, required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' }
});
const Author = mongoose.model('Author', authorSchema);
const Post = mongoose.model('Post', postSchema);
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' } stores only the author’s _id inside each post, ref: 'Author' tells Mongoose which model that ID refers to, this is the direct Mongoose equivalent of Prisma’s authorId Int foreign key (Module 5).
Creating Referenced Documents
const author = await Author.create({ name: 'Robert Martin' });
const post = await Post.create({
title: 'Clean Code Principles',
body: 'Some thoughts on writing maintainable software.',
author: author._id
});
console.log(post);
{
_id: new ObjectId('...'),
title: 'Clean Code Principles',
body: 'Some thoughts on writing maintainable software.',
author: new ObjectId('65f1...'),
__v: 0
}
Notice author is just an ObjectId, not the full author document, exactly like a foreign key column holding just an integer in PostgreSQL.
Loading the Referenced Document With populate
const post = await Post.findOne({ title: 'Clean Code Principles' }).populate('author');
console.log(post);
{
_id: new ObjectId('...'),
title: 'Clean Code Principles',
body: 'Some thoughts on writing maintainable software.',
author: { _id: new ObjectId('65f1...'), name: 'Robert Martin', __v: 0 },
__v: 0
}
.populate('author') replaces the raw ObjectId with the actual author document, in one call, this is Mongoose’s version of Prisma’s include (Module 5, Lesson 3), and without it, MongoDB would need a second, separate query to fetch the author.
Referencing the Other Direction
const authorSchema = new mongoose.Schema({
name: { type: String, required: true },
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Post' }]
});
An Author can hold an array of post ObjectIds, and .populate('posts') loads every referenced post at once, the reverse direction of the relationship, same pattern as Author.books in Prisma (Module 5, Lesson 1).
Try It
- Define
AuthorandPostmodels with a reference, as shown above. - Create an author, then a post referencing that author’s
_id. - Fetch the post with
.populate('author'), and confirm the full author document appears nested inside, not just an ID. - Fetch a post without
.populate(), and confirmauthoris just a rawObjectId. Explain, in your own words, whatpopulateactually does.
Recap
- Referencing stores another document’s
_id, withref: 'ModelName'telling Mongoose which model it points to, the MongoDB equivalent of a foreign key. .populate(fieldName)loads the referenced document in place of the raw ID, MongoDB’s version of Prisma’sinclude.- Referencing fits data that’s shared, reused, or queried independently of its relationship, unlike the embedded comments from the last lesson.
Next lesson: choosing between embedding and referencing for a given relationship.