Beyond the basic field rules from the last lesson, Mongoose supports richer validation and a clean way to work with referenced documents.
const productSchema = new Schema({
name: { type: String, required: true, minlength: 2, maxlength: 100 },
price: { type: Number, required: true, min: 0 },
category: { type: String, enum: ["electronics", "books", "clothing"] }
})A validate function runs your own logic and returns true or false:
const userSchema = new Schema({
email: {
type: String,
required: true,
validate: {
validator: (value) => /^.+@.+\..+$/.test(value),
message: "Enter a valid email address"
}
}
})To reference another model — the "referencing" pattern from the schema-design lesson — set a field's type to Schema.Types.ObjectId and point ref at the target model's name:
const postSchema = new Schema({
title: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: "User" }
})Without populate(), a query returns just the raw ObjectId in the author field. Calling .populate("author") replaces it with the full referenced document — the same result $lookup produces in the aggregation pipeline, with a simpler API:
const post = await Post.findOne({ title: "Getting Started" }).populate("author")
console.log(post.author.name)
// "Rahul Verma" — the full User document is now attachedpopulate() vs $lookup — different trade-offs
populate() runs as a separate query behind the scenes, joined in application code rather than inside the database — for simple cases this is fine and much easier to read; for complex reporting queries across large collections, the aggregation pipeline's $lookup is usually the faster choice.