Chapter 72 โ Mongoose Schemas
๐ Definition
A Mongoose schema defines the structure, types, defaults, validations, and hooks for documents in a MongoDB collection. The model is the class derived from the schema that you use to perform CRUD.
๐ Why Schemas in a "Schemaless" DB?
MongoDB is flexible by design, but in production you want predictable shapes:
- Type safety (no
nullstrings, no string-where-number). - Validation at write time.
- Default values, timestamps, soft deletes.
- Pre/post hooks (hashing passwords, syncing timestamps).
๐ป Code Example โ Basic Schema
import mongoose from "mongoose";
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
password: { type: String, required: true, select: false },
name: { type: String, required: true },
age: { type: Number, min: 0, max: 120 },
role: { type: String, enum: ["admin", "user"], default: "user" },
tags: { type: [String], default: [] },
isActive: { type: Boolean, default: true },
}, {
timestamps: true, // adds createdAt + updatedAt
});
export const User = mongoose.model("User", userSchema);๐ป Code Example โ Nested Schema
const addressSchema = new mongoose.Schema({
street: String,
city: String,
country: String,
}, { _id: false });
const userSchema = new mongoose.Schema({
name: String,
address: addressSchema, // embedded
addresses: [addressSchema], // embedded array
});๐ป Code Example โ References (Foreign Keys)
const orderSchema = new mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
items: [{
product: { type: mongoose.Schema.Types.ObjectId, ref: "Product" },
quantity: Number,
price: Number,
}],
status: { type: String, enum: ["pending", "paid", "shipped"], default: "pending" },
});
export const Order = mongoose.model("Order", orderSchema);
// Usage with populate
const order = await Order.findById(id)
.populate("user", "name email")
.populate("items.product", "name price");๐ป Code Example โ Validation
const productSchema = new mongoose.Schema({
name: { type: String, required: [true, "Name is required"], minlength: 3 },
price: { type: Number, required: true, min: [0, "Price cannot be negative"] },
sku: {
type: String,
required: true,
validate: {
validator: (v) => /^[A-Z]{3}-\d{4}$/.test(v),
message: "SKU must look like ABC-1234",
},
},
});Invalid writes throw a ValidationError:
try {
await Product.create({ name: "x", price: -10, sku: "bad" });
} catch (err) {
console.log(err.errors); // structured per-field errors
}๐ป Code Example โ Hooks (Middleware)
import bcrypt from "bcrypt";
userSchema.pre("save", async function (next) {
if (!this.isModified("password")) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
userSchema.pre(["findOneAndUpdate", "updateOne"], function (next) {
const update = this.getUpdate();
if (update.password) {
bcrypt.hash(update.password, 10).then(h => {
this.setUpdate({ ...update, password: h });
next();
});
} else next();
});
userSchema.post("save", function (doc, next) {
console.log(`User ${doc.id} saved`);
next();
});๐ป Code Example โ Instance & Static Methods
// Instance method โ on a single document
userSchema.methods.checkPassword = async function (raw) {
return bcrypt.compare(raw, this.password);
};
// Static method โ on the model
userSchema.statics.findByEmail = function (email) {
return this.findOne({ email });
};
// Usage
const user = await User.findByEmail("a@b.com").select("+password");
const ok = await user.checkPassword("input");๐ป Code Example โ Virtuals (Computed Fields)
userSchema.virtual("fullName").get(function () {
return `${this.firstName} ${this.lastName}`;
});
userSchema.set("toJSON", { virtuals: true }); // include in JSON output
// Usage
user.fullName; // "Soumya Rout"๐ป Code Example โ Indexes Defined on Schema
userSchema.index({ email: 1 }); // single field
userSchema.index({ role: 1, createdAt: -1 }); // compound
userSchema.index({ name: "text" }); // text search
userSchema.index({ tokenExpires: 1 }, { expireAfterSeconds: 0 }); // TTLSee Chapter 47 โ Indexing for full details.
๐ป Code Example โ Discriminators (Polymorphism)
const baseOpts = { discriminatorKey: "kind", collection: "events" };
const Event = mongoose.model("Event", new mongoose.Schema({ time: Date }, baseOpts));
const ClickEvent = Event.discriminator("Click", new mongoose.Schema({ url: String }));
const SignupEvent = Event.discriminator("Signup", new mongoose.Schema({ source: String }));
await ClickEvent.create({ time: new Date(), url: "/home" });
await SignupEvent.create({ time: new Date(), source: "ad-campaign" });
// Find both kinds in one query:
await Event.find();
// Find only clicks:
await ClickEvent.find();๐ป Code Example โ NestJS Mongoose Module
// schemas/user.schema.ts
@Schema({ timestamps: true })
export class User {
@Prop({ required: true, unique: true, lowercase: true })
email: string;
@Prop({ required: true, select: false })
password: string;
@Prop({ default: "user", enum: ["admin", "user"] })
role: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
// users.module.ts
@Module({
imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
})
export class UsersModule {}
// users.service.ts
@Injectable()
export class UsersService {
constructor(@InjectModel(User.name) private userModel: Model<User>) {}
findByEmail(email: string) {
return this.userModel.findOne({ email });
}
}๐ Real-World Impact
- Schemas catch bad data before it lands in your DB.
- Hooks centralize cross-cutting concerns like hashing or audit logs.
- Virtuals keep derived data out of the storage layer.
๐ฏ Likely Interview Questions
- What is Mongoose? โ An ODM (Object Data Modelling) library for Node.js on top of the MongoDB driver: schemas, type casting, validation, middleware,
populate, and model methods. - Why use schemas with a NoSQL DB? โ The database is flexible, but the application needs predictable shapes: validation at write time, defaults, hooks, and fewer
undefinedbugs. - Difference between
preandposthooks? โpreruns before the operation and can modify data or abort it (throw);postruns after and receives the result โ good for logging, cache invalidation, events. - How would you hash a password before saving? โ
pre("save")hook: skip if!this.isModified("password"), elsethis.password = await bcrypt.hash(this.password, 12); keepselect: falseon the field. Remembersavehooks don't run onupdateOne/findOneAndUpdate. - What are virtuals? โ Computed properties (e.g.
fullName) that are not stored in MongoDB; include them in output withtoJSON: { virtuals: true }. Virtual populate can also link related collections without storing ids. - Document middleware vs query middleware? โ Document middleware (
save,validate,deleteOneon a doc) hasthis= document; query middleware (find,updateOne,findOneAndUpdate) hasthis= query. Updates via queries skipsavehooks. - What is
select: false? โ Field is excluded from query results by default; opt in with.select("+password"). - What are discriminators? โ Mongoose's way of storing several schemas (types) in one collection with a
kindkey โ e.g. different event types sharing base fields. - What is
timestamps: true? โ Adds and maintainscreatedAtandupdatedAtautomatically. - Instance methods vs statics? โ Methods operate on one document (
user.checkPassword()); statics on the model (User.findByEmail()).
โ Data Modelling & Schema Patterns | Index | Next: Indexing โ