Getting started with Mongoose virtuals

Mongoose virtuals are document properties you can get and set but are not saved in MongoDB. These properties are computed whenever you access them.

Virtual properties are used for formatting and combining fields and de-composing a single value into multiple values before storing it in the collection.

A simple use case for Mongoose's virtual property is the full name consisting of first and last names.

Let us say that you have got the following Mongoose schema:

const mongoose = require('mongoose')
const { Schema } = mongoose

// Define a schema
const userSchema = new Schema({
  name: {
    first: String,
    last: String
  },
  age: Number
})

// Define a model
const User = mongoose.model('User', userSchema)

// Create a document
const alex = new User({
  name: { first: 'Alex', last: 'Jones' },
  age: 32
})

Now you want to get the full name of alex, you can do this manually by concatenating the first and last name properties:

console.log(`${alex.name.first} ${alex.name.last}`)    // Alex Jones

Defining a virtual property

Instead of combining the first and last name every time, a better approach would be to define a virtual property called fullName. Mongoose provides two types of virtual methods: GET and SET.

GET Method

Virtual get methods are useful for formatting and combining document fields. You can also do any other complex processing before returning a value:

// Define setter virtual method
userSchema.virtual('fullName').get(function () {
  return `${this.name.first} ${this.name.last}`
})

Now whenever you access the fullName property, Mongoose will invoke your virtual method and return the result:

console.log(alex.fullName)    // Alex Jones

SET Method

The setter virtual methods are used to split strings into sub-strings and do other operations.

The following example demonstrates how you can split the name property into first and last name at any whitespace:

// Define setter virtual method
userSchema.virtual('fullName').set(function (name) {
  const tokens = name.split(' ')
  this.name.first = tokens[0]
  this.name.last = tokens[1]
})

Now, whenever you assign a value to the fullName property, it will be automatically split into first and last names:

const john = new User({
  fullName: 'John Doe',
  age: 32
})

console.log(john.name.first) // John
console.log(john.name.last) // Doe

Mongoose virtual setters are applied before any other validation. So the above example would still work even if the first and last properties were required.

Queries and field selection

Mongoose virtuals are just computed properties and are not available for queries and field selection. For example, you can not use the fullName property to select a user from the database:

// This query won't return any result
User.findOne({
  fullName: 'Alex Jones'
})
  .then(alex => console.log(alex))
  .catch(err => console.log(err))

Only non-virtual properties work for queries and field selections. Since virtuals are not stored in MongoDB, you can not use them in queries.

Including virtuals in JSON

By default, Mongoose ignores virtual properties when you convert a document into a JSON string using either toJSON() or toObject():

// console.log(john.toJSON());
{
  name: { first: 'John', last: 'Doe' },
  _id: 5fd9b24943e7fa3e6ecf6a3f,
  age: 32
}

However, you can pass { virtuals: true } to the toJSON() method to include virtuals properties as well:

// console.log(john.toJSON({ virtuals: true }))
{
  name: { first: 'John', last: 'Doe' },
  _id: 5fd9b24943e7fa3e6ecf6a3f,
  age: 32,
  fullName: 'John Doe',
  id: '5fd9b24943e7fa3e6ecf6a3f'
}

Alternatively, you could also configure Mongoose to automatically include virtual properties for all documents:

mongoose.set('toJSON', { virtuals: true })

This is another reason you should always use virtuals instead of instance methods in Mongoose if you want to include them in JSON.

Read this article to learn more about Mongoose schemas.

✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.

You might also like...

Digital Ocean

The simplest cloud platform for developers & teams. Start with a $200 free credit.

Buy me a coffee ☕

If you enjoy reading my articles and want to help me out paying bills, please consider buying me a coffee ($5) or two ($10). I will be highly grateful to you ✌️

Enter the number of coffees below:

✨ Learn to build modern web applications using JavaScript and Spring Boot

I started this blog as a place to share everything I have learned in the last decade. I write about modern JavaScript, Node.js, Spring Boot, core Java, RESTful APIs, and all things web development.

The newsletter is sent every week and includes early access to clear, concise, and easy-to-follow tutorials, and other stuff I think you'd enjoy! No spam ever, unsubscribe at any time.