Go beyond basic CRUD with Mongoose — virtual properties, instance methods, static methods, pre/post hooks, and query helpers.

Abdur Razzak
Full-Stack Web Developer
Most developers use Mongoose for basic find, save, and delete operations. But Mongoose has powerful features that keep your data layer clean and DRY: virtuals for computed properties, instance methods for document behavior, static methods for model-level queries, middleware (hooks) for pre/post operations, and query helpers for reusable query conditions. Mastering these makes your MongoDB codebase significantly more maintainable.
Virtuals are document properties not stored in MongoDB — they are computed from other fields. Define a virtual on the schema: UserSchema.virtual('fullName').get(function() { return this.firstName + ' ' + this.lastName; }). Virtuals appear in JavaScript objects but not in database documents. Enable them in JSON output with { toJSON: { virtuals: true } } in your schema options.
Instance methods are functions available on individual document instances. Add them to schema.methods: UserSchema.methods.comparePassword = async function(candidatePassword) { return bcrypt.compare(candidatePassword, this.password); }. Call them on a retrieved document: const isMatch = await user.comparePassword(inputPassword). Instance methods are ideal for operations that logically belong to a specific document.
Static methods are called on the Model itself, not on instances. Add them to schema.statics: UserSchema.statics.findByEmail = function(email) { return this.findOne({ email }); }. Call them as User.findByEmail('test@test.com'). Statics are perfect for custom query methods that are used frequently throughout the codebase and should live close to the model definition.
Mongoose middleware runs before (pre) or after (post) document operations. The most common use case: hash passwords before saving: UserSchema.pre('save', async function(next) { if (this.isModified('password')) { this.password = await bcrypt.hash(this.password, 12); } next(); }). Post hooks are useful for logging, cache invalidation, or triggering related operations after a save.
Query helpers add chainable methods to Mongoose queries. Define them in schema.query: BlogSchema.query.published = function() { return this.where({ isPublished: true }); }. Now you can chain: Blog.find().published().sort('-publishedAt'). Query helpers DRY up common filter conditions that appear across many routes, ensuring consistent application of business rules at the query level.