Stop Storing Redundant Data: Using Mongoose Virtuals for Computed Properties
Stop syncing redundant fields in MongoDB. Learn how to use Mongoose Virtuals to create computed properties that simplify your API without bloating your database.
29 Jun 2026, 12:13 UTC

The Cost of Data Redundancy
When designing a MongoDB schema, it is tempting to store every possible variation of a data point to make querying easier. For example, you might store firstName, lastName, and a pre-concatenated fullName. While this seems efficient, it creates a synchronization nightmare: every time a user updates their last name, you must remember to update the full name field across every single document.
The solution is to move computed logic out of the database and into the application layer using Mongoose Virtuals. Virtuals are document properties that you can get and set but that are not persisted to MongoDB. They allow you to maintain a single source of truth in your database while providing a convenient interface for your API.
Implementing Virtual Getters
A virtual getter acts like a computed property. It calculates a value on the fly whenever the property is accessed. This is ideal for formatting data, combining fields, or calculating totals based on existing document values.
To implement a virtual, you use the schema.virtual() method. This defines the property name and a function that returns the desired value based on the current state of the document (accessed via this).
Example: User Profile Concatenation
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userSchema = new Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true }
}, {
// Crucial: Virtuals are hidden by default during serialization
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Define the virtual property 'fullName'
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
const User = mongoose.model('User', userSchema);Exposing Virtuals to Your API
A common point of frustration for developers is seeing a virtual work perfectly in a console.log(user.fullName) call, but seeing it disappear when the document is sent as a JSON response in an Express route.
By default, Mongoose does not include virtuals when converting a document to a plain JavaScript object or a JSON string. To fix this, you must set the toJSON and toObject options in the schema configuration to virtuals: true. Without these settings, your API clients will never see the computed fields, as they only exist in the Mongoose document instance, not the raw MongoDB BSON.
The Critical Trade-off: Queryability
Virtuals provide a clean API, but they come with a significant limitation: they do not exist in the database.
Because MongoDB is unaware of the virtual property, you cannot use it in a query filter. For example, if you have a fullName virtual, the following operation will fail to return any results:
// This will NOT work
const users = await User.find({ fullName: 'Jane Doe' });If you need to search or sort by a computed value, you have two choices: use a MongoDB Aggregation pipeline to compute the value on the server side, or revert to storing the value physically in the database (denormalization). Use virtuals for presentation logic and physical fields for searchable data.
Performance Warning
Avoid placing heavy computational logic (like complex loops or external API calls) inside a virtual getter. Since virtuals are often triggered during serialization (e.g., when calling res.json(users)), a slow getter can exponentially increase the response time of an endpoint returning a large array of documents.
Verification Checklist
- Check Persistence: Save a document and check the MongoDB shell. The virtual property should be absent from the stored document.
- Check Serialization: Call
JSON.stringify(doc). If the virtual is missing, verify thattoJSON: { virtuals: true }is in the schema options. - Check Querying: Attempt a
.find()using the virtual key. It should return an empty array, confirming the property is not persisted.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.