Mongoose Virtuals: Derived Fields That Shouldn't Live in MongoDB
Mongoose virtuals let you define computed properties like fullName without storing them in MongoDB, but they are hidden from JSON output unless you enable schema serialization.
09 Mar 2026, 18:40 UTC

The Problem: Derived Data That Keeps Drifting Out of Sync
Your user profile stores firstName and lastName as separate fields, but the frontend needs a single display name. You could concatenate the strings in every route handler, or store a fullName field and risk it drifting out of sync when someone edits only the first name. Mongoose virtuals define a computed property on the schema that exists in your Node.js application but is never written to MongoDB.
Defining a Virtual Getter
A virtual is a schema property with a getter function, and optionally a setter. It does not correspond to a column in the database. This example adds fullName and age to a user schema:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true },
birthDate: { type: Date },
}, {
toJSON: { virtuals: true },
toObject: { virtuals: true },
});
userSchema.virtual('fullName').get(function () {
return `${this.firstName} ${this.lastName}`.trim();
});
userSchema.virtual('age').get(function () {
if (!this.birthDate) return undefined;
const today = new Date();
const birth = new Date(this.birthDate);
let years = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
years -= 1;
}
return years;
});
const User = mongoose.model('User', userSchema);
The schema options toJSON and toObject control what happens when Mongoose converts a document to a plain JavaScript object. Without them, virtuals are omitted by default.
Worked Example: Exposing Virtuals in an API Response
Run this in a Node.js application where Mongoose is installed and connected to MongoDB. No special operating-system permissions are required. Replace <MONGODB_URI> with your connection string.
mongoose.connect('');
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id).exec();
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
Because toJSON: { virtuals: true } is set on the schema, Express's res.json(user) serializes the document and includes fullName and age in the HTTP response. The MongoDB document itself still contains only firstName, lastName, and birthDate.
Practical check: inspect the raw object and the JSON output separately.
const raw = user.toObject();
console.log('raw.fullName:', raw.fullName); // undefined
const serialized = user.toJSON();
console.log('serialized.fullName:', serialized.fullName); // 'Ada Lovelace'
Trade-offs and Limitations
- Not queryable. You cannot run
User.find({ fullName: 'Ada Lovelace' })because MongoDB has no index or stored value for the virtual. If you need to filter or sort by the derived value, store it as a real field and keep it synchronized in apre('save')hook instead. - Not indexed. Virtuals do not support MongoDB indexes or unique constraints.
- Serialization cost. Getters run every time
toJSONortoObjectis called. Keep them lightweight; avoid database calls or heavy computation inside a virtual getter. - Setters are optional and can hide validation. A virtual setter can split
fullNameback intofirstNameandlastName, but schema validation still runs on those underlying fields duringsave.
Actionable Closing
Use Mongoose virtuals for read-only, UI-only data that is cheap to compute from existing fields. Enable toJSON and toObject virtuals when your API sends Mongoose documents directly to clients. If the derived value must be queryable, indexed, or enforced by the database, promote it to a real schema field and maintain it with middleware rather than a virtual.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.