Virtuals in Mongoose: Explained with Examples

Updated: December 30, 2023 By: Guest Contributor Post a comment

Introduction

In modern web development, using databases is an integral part of storing, retrieving, and handling data. When building applications using Node.js and MongoDB, Mongoose is a powerful Object Data Modeling (ODM) library that creates a bridge between MongoDB and the Node.js runtime. Mongoose simplifies the tasks of working with MongoDB collections and documents through schemas and provides numerous features that enhance the modeling capabilities, one of which is the use of ‘virtuals’.

Virtuals in Mongoose are schema properties that do not get persisted to the MongoDB collection. They are typically used for computed properties on documents, allowing us to add fields that can be populated on the fly based on other field values. Virtual properties don’t affect the underlying database, which makes them very powerful for formatting or combining fields without altering the raw data. In this tutorial, we will explore what virtuals are, how to define them, and several use-cases with practical examples.

Using ES6 features like arrow functions and async/await alongside ES modules, our examples will aim to bring these advanced concepts into day-to-day application for a modern development workflow.

Setting Up the Environment

Before we jump into virtuals, you need to have a basic environment set up. You should have Node.js installed, a MongoDB database accessible locally or remotely, and a new Node.js project initialized with ‘npm init’. After initializing your project, install Mongoose using ‘npm install mongoose’, then create a new file (e.g., ‘mongooseVirtuals.js’).

To work with ES modules, ensure that you either name your project files with a ‘.mjs’ extension or include `{“type”: “module”}` in your ‘package.json’. Let’s start by importing Mongoose:

import mongoose from 'mongoose';

Then, connect to your database:

await mongoose.connect('mongodb://localhost:27017/yourDatabase', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

Basic Usage of Virtuals

Let us define a simple User schema with first and last name and see how a virtual property can be used to compute a full name. We will not save the full name directly to the MongoDB collection:

const userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String,
});

userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

const User = mongoose.model('User', userSchema);

In the example above, ‘fullName’ is a virtual property. The ‘get’ method defines a getter which lets us access ‘fullName’ as if it were a regular property of our model instances:

let user = new User({
  firstName: 'John',
  lastName: 'Doe'
});
console.log(user.fullName); // Output: 'John Doe'

Note that we use a function declaration instead of an arrow function in ‘get()’ because we need to access the ‘this’ context within it, pointing to the instance of the document.

Advanced Usage and Techniques

Beyond the basics, you can also define virtuals for setting values. Here’s an example of how to split a full name into first and last names:

userSchema.virtual('fullName').set(function (name) {
  let [firstName, lastName] = name.split(' ');
  this.firstName = firstName;
  this.lastName = lastName;
});

This enables :

user.fullName = 'Jane Smith';
console.log(user.firstName); // Output: 'Jane'
console.log(user.lastName); // Output: 'Smith'

Another advanced concept is using virtuals with Mongoose query population. Population is a means of automatically replacing specified paths in the document with documents from other collections. Defining virtuals for related documents wouldn’t store relations in MongoDB, but allow us to work with them seamlessly:

const postSchema = new mongoose.Schema({
  title: String,
  _author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }
});

postSchema.virtual('author', {
  ref: 'User',
  localField: '_author',
  foreignField: '_id',
  justOne: true
});

const Post = mongoose.model('Post', postSchema);

Important: When querying, to include virtuals set ‘virtuals: true’ option in toObject() or toJSON() methods call otherwise the virtuals will not be included. Like this:

let post = await Post.findById(postId).populate('author').exec();
console.log(post.author.fullName); // Provided the author is populated and the User schema has a fullName virtual

Summary

Throughout this tutorial, we have taken a look at virtuals in Mongoose and addressed their basic as well as advanced applications, enhancing our schema models with properties that aren’t stored in the DB but offer flexibility and object-oriented advantages in manipulating document data. Employing these Mongoose virtuals with modern JavaScript features further streamlines development making our code more elegant, readable, and efficient.

Remember, virtuals are just one aspect of what makes Mongoose powerful. As you build more complex web applications, you’ll find many opportunities to implement and benefit from virtuals throughout your data models. Happy coding!