After this lesson, you will be able to: Use Mongoose: define schemas, validation, middleware (pre/post hooks), virtuals.
Mongoose is the standard Node.js ODM (object document mapper). Adds schemas, validation, hooks, virtuals to raw Mongo. Used by ~80% of production Node + Mongo apps.
Define structure + types.
// npm i mongooseimport mongoose, { Schema, model } from 'mongoose';await mongoose.connect(process.env.MONGO_URI!);const userSchema = new Schema({email: { type: String, required: true, unique: true, lowercase: true, trim: true },name: { type: String, required: true, minLength: 1, maxLength: 100 },age: { type: Number, min: 0, max: 150 },role: { type: String, enum: ['student', 'tutor', 'admin'], default: 'student' },tags: [{ type: String }],address: {city: String,zip: String,},createdAt: { type: Date, default: Date.now },});export const User = model('User', userSchema);// CRUDawait User.updateOne({ _id: u._id }, { $set: { age: 31 } });await User.deleteOne({ _id: u._id });
Built-in + custom validators.
const emailSchema = new Schema({email: {type: String,required: [true, 'Email is required'],validate: {validator: (v: string) => /^[^@]+@[^@]+\.[^@]+$/.test(v),message: 'Invalid email format',},},age: {type: Number,validate: {validator: Number.isInteger,message: 'Age must be an integer',},},});// Throws ValidationError on save if invalidtry {await User.create({ email: 'not-an-email', age: 1.5 });} catch (e) {if (e.name === 'ValidationError') {// handle}}
pre / post around save, find, etc.
import bcrypt from 'bcryptjs';const userSchema = new Schema({email: String,password: String,});// Hash password before saveuserSchema.pre('save', async function (next) {if (!this.isModified('password')) return next();this.password = await bcrypt.hash(this.password, 12);next();});// Soft-delete pattern via query middlewareuserSchema.pre(/^find/, function (next) {// 'this' is the query, filter out deletedthis.where({ deletedAt: null });next();});
Computed fields + reference resolution.
// Virtual field, not stored, computed on accessuserSchema.virtual('fullName').get(function () {return `${this.firstName} ${this.lastName}`;});// Reference + populate (Mongoose's $lookup wrapper)const postSchema = new Schema({title: String,author: { type: Schema.Types.ObjectId, ref: 'User' },});const Post = model('Post', postSchema);// Without populate, author is just an ObjectIdconst post = await Post.findById(id);// With populate, author becomes the full user docconst withAuthor = await Post.findById(id).populate('author');console.log(withAuthor.author.name);
Skipping schema validation, Mongo becomes garbage in months. Pre-save password hash without `this.isModified` check, hashes the hash on every save. Populate everything by reflex (N+1 across collections). Using Mongoose's `_id` virtual as primary key in API responses, better to use _id directly or transform.
Sign in and purchase access to unlock this lesson.