mongoose Documentation

MongoDB object modeling designed to work in an asynchronous environment.

27,475 stars JavaScript JavaScript 1 file ยท ~17,291 tokens #mongo#mongodb#nodejs#odm#orm
RAW Doc

File: README.md

Mongoose

Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment. Mongoose supports Node.js and Deno (alpha).




Documentation

The official documentation website is mongoosejs.com.

Mongoose 9.0.0 was released on November 21, 2025. You can find more details on backwards breaking changes in 9.0.0 on our docs site.

Support

Plugins

Check out the plugins search site to see hundreds of related modules from the community. Next, learn how to write your own plugin from the docs or this blog post.

Contributors

Pull requests are always welcome! Please base pull requests against the master
branch and follow the contributing guide.

If your pull request makes documentation changes, please do not
modify any .html files. The .html files are compiled code, so please make
your changes in docs/*.pug, lib/*.js, or test/docs/*.js.

View all 400+ contributors.

Installation

First install Node.js and MongoDB, then install the mongoose package using your preferred package manager:

Using npm

sh
npm install mongoose

Using pnpm

sh
pnpm add mongoose

Using Yarn

sh
yarn add mongoose

Using Bun

sh
bun add mongoose

Mongoose 6.8.0 also includes alpha support for Deno.

Importing

javascript
// Using Node.js `require()`
const mongoose = require('mongoose');

// Using ES6 imports
import mongoose from 'mongoose';

Or, using Deno's createRequire() for CommonJS support as follows.

javascript
import { createRequire } from 'https://deno.land/[email protected]/node/module.ts';
const require = createRequire(import.meta.url);

const mongoose = require('mongoose');

mongoose.connect('mongodb://127.0.0.1:27017/test')
  .then(() => console.log('Connected!'));

You can then run the above script using the following.

sh
deno run --allow-net --allow-read --allow-sys --allow-env mongoose-test.js

Mongoose Studio

Mongoose Studio is a free, fully open-source, browser-based MongoDB GUI built by the Mongoose team for apps that already use Mongoose. Install @mongoosejs/studio from npm and run it alongside your app as Express middleware, or deploy it on Vercel or Netlify, to browse and edit documents, query with your existing models and schemas with autocomplete, build dashboards, visualize and edit GeoJSON, and use AI-assisted MongoDB workflows without moving data into a hosted third-party workspace or sharing raw MongoDB connection strings.

Mongoose for Enterprise

Available as part of the Tidelift Subscription

The maintainers of mongoose and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. Learn more.

Overview

Connecting to MongoDB

First, we need to define a connection. If your app uses only one database, you should use mongoose.connect. If you need to create additional connections, use mongoose.createConnection.

Both connect and createConnection take a mongodb:// URI, or the parameters host, database, port, options.

js
await mongoose.connect('mongodb://127.0.0.1/my_database');

Once connected, the open event is fired on the Connection instance. If you're using mongoose.connect, the Connection is mongoose.connection. Otherwise, mongoose.createConnection return value is a Connection.

Note: If the local connection fails then try using 127.0.0.1 instead of localhost. Sometimes issues may arise when the local hostname has been changed.

Important! Mongoose buffers all the commands until it's connected to the database. This means that you don't have to wait until it connects to MongoDB in order to define models, run queries, etc.

Defining a Model

Models are defined through the Schema interface.

js
const Schema = mongoose.Schema;
const ObjectId = Schema.ObjectId;

const BlogPost = new Schema({
  author: ObjectId,
  title: String,
  body: String,
  date: Date
});

Aside from defining the structure of your documents and the types of data you're storing, a Schema handles the definition of:

The following example shows some of these features:

js
const Comment = new Schema({
  name: { type: String, default: 'hahaha' },
  age: { type: Number, min: 18, index: true },
  bio: { type: String, match: /[a-z]/ },
  date: { type: Date, default: Date.now },
  buff: Buffer
});

// a setter
Comment.path('name').set(function(v) {
  return capitalize(v);
});

// middleware
Comment.pre('save', function(next) {
  notify(this.get('email'));
  next();
});

Accessing a Model

Once we define a model through mongoose.model('ModelName', mySchema), we can access it through the same function

js
const MyModel = mongoose.model('ModelName');

Or just do it all at once

js
const MyModel = mongoose.model('ModelName', mySchema);

The first argument is the singular name of the collection your model is for. Mongoose automatically looks for the plural version of your model name. For example, if you use

js
const MyModel = mongoose.model('Ticket', mySchema);

Then MyModel will use the tickets collection, not the ticket collection. For more details read the model docs.

Once we have our model, we can then instantiate it, and save it:

js
const instance = new MyModel();
instance.my.key = 'hello';
await instance.save();

Or we can find documents from the same collection

js
await MyModel.find({});

You can also findOne, findById, update, etc.

js
const instance = await MyModel.findOne({ /* ... */ });
console.log(instance.my.key); // 'hello'

For more details check out the docs.

Important! If you opened a separate connection using mongoose.createConnection() but attempt to access the model through mongoose.model('ModelName') it will not work as expected since it is not hooked up to an active db connection. In this case access your model through the connection you created:

js
const conn = mongoose.createConnection('your connection string');
const MyModel = conn.model('ModelName', schema);
const m = new MyModel();
await m.save(); // works

vs

js
const conn = mongoose.createConnection('your connection string');
const MyModel = mongoose.model('ModelName', schema);
const m = new MyModel();
await m.save(); // does not work b/c the default connection object was never connected

Embedded Documents

In the first example snippet, we defined a key in the Schema that looks like:

txt
comments: [Comment]

Where Comment is a Schema we created. This means that creating embedded documents is as simple as:

js
// retrieve my model
const BlogPost = mongoose.model('BlogPost');

// create a blog post
const post = new BlogPost();

// create a comment
post.comments.push({ title: 'My comment' });

await post.save();

The same goes for removing them:

js
const post = await BlogPost.findById(myId);
post.comments[0].deleteOne();
await post.save();

Embedded documents enjoy all the same features as your models. Defaults, validators, middleware.

Middleware

See the docs page.

Intercepting and mutating method arguments

You can intercept method arguments via middleware.

For example, this would allow you to broadcast changes about your Documents every time someone sets a path in your Document to a new value:

js
schema.pre('set', function(next, path, val, typel) {
  // `this` is the current Document
  this.emit('set', path, val);

  // Pass control to the next pre
  next();
});

Moreover, you can mutate the incoming method arguments so that subsequent middleware see different values for those arguments. To do so, just pass the new values to next:

js
schema.pre(method, function firstPre(next, methodArg1, methodArg2) {
  // Mutate methodArg1
  next('altered-' + methodArg1.toString(), methodArg2);
});

// pre declaration is chainable
schema.pre(method, function secondPre(next, methodArg1, methodArg2) {
  console.log(methodArg1);
  // => 'altered-originalValOfMethodArg1'

  console.log(methodArg2);
  // => 'originalValOfMethodArg2'

  // Passing no arguments to `next` automatically passes along the current argument values
  // i.e., the following `next()` is equivalent to `next(methodArg1, methodArg2)`
  // and also equivalent to, with the example method arg
  // values, `next('altered-originalValOfMethodArg1', 'originalValOfMethodArg2')`
  next();
});

Schema gotcha

type, when used in a schema has special meaning within Mongoose. If your schema requires using type as a nested property you must use object notation:

js
new Schema({
  broken: { type: Boolean },
  asset: {
    name: String,
    type: String // uh oh, it broke. asset will be interpreted as String
  }
});

new Schema({
  works: { type: Boolean },
  asset: {
    name: String,
    type: { type: String } // works. asset is an object with a type property
  }
});

Driver Access

Mongoose is built on top of the official MongoDB Node.js driver. Each mongoose model keeps a reference to a native MongoDB driver collection. The collection object can be accessed using YourModel.collection. However, using the collection object directly bypasses all mongoose features, including hooks, validation, etc. The one
notable exception is that YourModel.collection still buffers
commands. As such, YourModel.collection.find() will not
return a cursor.

API Docs

Mongoose API documentation, generated using dox
and acquit.

MongoDB Runners

Unofficial CLIs

Data Seeding

Express Session Stores

License

Copyright (c) 2010 LearnBoost <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


File: docs/tutorials/custom-casting.md

Custom Casting

Mongoose 5.4.0 introduced several ways to configure SchemaTypes globally.
One of these new features is the SchemaType.cast() function, which enables you to override Mongoose's built-in casting.

For example, by default Mongoose will throw an error if you attempt to cast
a string that contains a Japanese numeral to a number.

javascript acquit:custom casting.*casting error
const schema = new mongoose.Schema({
  age: Number
});
const Model = mongoose.model('Test', schema);

const doc = new Model({ age: 'ไบŒ' });
const err = doc.validateSync();
// "Cast to Number failed for value "ไบŒ" at path "age""
err.message;

You can overwrite the default casting function for numbers to allow converting
the string that contains the Japanese numeral "2" to a number as shown below.

javascript acquit:custom casting.*casting override
// Calling `cast()` on a class that inherits from `SchemaType` returns the
// current casting function.
const originalCast = mongoose.Number.cast();

// Calling `cast()` with a function sets the current function used to
// cast a given schema type, in this cast Numbers.
mongoose.Number.cast(v => {
  if (v === 'ไบŒ') {
    return 2;
  }
  return originalCast(v);
});

const schema = new mongoose.Schema({
  age: Number
});

const Model = mongoose.model('Test', schema);

const doc = new Model({ age: 'ไบŒ' });
const err = doc.validateSync();
err; // null
doc.age; // 2

File: docs/tutorials/dates.md

Working With Dates

Here's how you declare a path of type Date with a Mongoose schema:

javascript
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: String,
  // `lastActiveAt` is a date
  lastActiveAt: Date
});
const User = mongoose.model('User', userSchema);

When you create a user document, Mongoose will cast
the value to a native JavaScript date
using the Date() constructor.

javascript acquit:date tutorial.*example 1.2
const user = new User({
  name: 'Jean-Luc Picard',
  lastActiveAt: '2002-12-09'
});
user.lastActiveAt instanceof Date; // true

An invalid date will lead to a CastError when you validate the document.

javascript acquit:date tutorial.*example 1.3
const user = new User({
  name: 'Jean-Luc Picard',
  lastActiveAt: 'not a date'
});
user.lastActiveAt instanceof Date; // false
user.validateSync().errors['lastActiveAt']; // CastError

Validators

Dates have two built-in validators: min and max. These validators will
report a ValidatorError if the given date is strictly less than min or
strictly greater than max.

javascript acquit:date tutorial.*example 1.2.1
const episodeSchema = new mongoose.Schema({
  title: String,
  airedAt: {
    type: Date,
    // The dates of the first and last episodes of
    // Star Trek: The Next Generation
    min: '1987-09-28',
    max: '1994-05-23'
  }
});
const Episode = mongoose.model('Episode', episodeSchema);

const ok = new Episode({
  title: 'Encounter at Farpoint',
  airedAt: '1987-09-28'
});
ok.validateSync(); // No error

const bad = new Episode({
  title: 'What You Leave Behind',
  airedAt: '1999-06-02'
});
bad.airedAt; // "1999-06-02T00:00:00.000Z"

// Path `airedAt` (Tue Jun 01 1999 20:00:00 GMT-0400 (EDT)) is after
// maximum allowed value (Sun May 22 1994 20:00:00 GMT-0400 (EDT)).
bad.validateSync();

Querying

MongoDB supports querying by date ranges and sorting by dates. Here's some
examples of querying by dates, date ranges, and sorting by date:

javascript acquit:date tutorial.*example 1.3.1
// Find episodes that aired on this exact date
const episodes = await Episode.find({ airedAt: new Date('1987-10-26') });
episodes[0].title; // "Where No One Has Gone Before"
// Find episodes within a range of dates, sorted by date ascending
const sortedEpisodes = await Episode.
  find({ airedAt: { $gte: '1987-10-19', $lte: '1987-10-26' } }).
  sort({ airedAt: 1 });
sortedEpisodes[0].title; // "The Last Outpost"
sortedEpisodes[1].title; // "Where No One Has Gone Before"

Casting Edge Cases

Date casting has a couple small cases where it differs from JavaScript's
native date parsing. First, Mongoose looks for a valueOf() function on the given object,
and calls valueOf() before casting the date. This means Mongoose can cast
moment objects to dates automatically.

javascript acquit:date tutorial.*example 1.4.1
const moment = require('moment');
const user = new User({
  name: 'Jean-Luc Picard',
  lastActiveAt: moment.utc('2002-12-09')
});
user.lastActiveAt; // "2002-12-09T00:00:00.000Z"

By default, if you pass a numeric
string to the Date constructor, JavaScript will attempt to convert it to a
year.

javascript
new Date(1552261496289); // "2019-03-10T23:44:56.289Z"
new Date('1552261496289'); // "Invalid Date"
new Date('2010'); // 2010-01-01T00:00:00.000Z

Mongoose converts numeric strings that contain numbers outside the range of representable dates in JavaScript and converts them to numbers before passing them to the date constructor.

javascript acquit:date tutorial.*example 1.4.3
const user = new User({
  name: 'Jean-Luc Picard',
  lastActiveAt: '1552261496289'
});
user.lastActiveAt; // "2019-03-10T23:44:56.289Z"

Timezones

MongoDB stores dates as 64-bit integers, which
means that Mongoose does not store timezone information by default. When
you call Date#toString(), the JavaScript runtime will use your OS' timezone.


File: docs/tutorials/findoneandupdate.md

How to Use `findOneAndUpdate()` in Mongoose

The findOneAndUpdate() function in Mongoose has a wide variety of use cases. You should use save() to update documents where possible, for better validation and middleware support.
However, there are some cases where you need to use findOneAndUpdate(). In this tutorial, you'll see how to use findOneAndUpdate(), and learn when you need to use it.

Getting Started

As the name implies, findOneAndUpdate() finds the first document that matches a given filter, applies an update, and returns the document.
The findOneAndUpdate() function has the following signature:

javascript
function findOneAndUpdate(filter, update, options) {}

By default, findOneAndUpdate() returns the document as it was before update was applied.
In the following example, doc initially only has name and _id properties.
findOneAndUpdate() adds an age property, but the result of findOneAndUpdate() does not have an age property.

javascript acquit:tutorial.*findoneandupdate.*basic case
const Character = mongoose.model('Character', new mongoose.Schema({
  name: String,
  age: Number
}));

const _id = new mongoose.Types.ObjectId('0'.repeat(24));
let doc = await Character.create({ _id, name: 'Jean-Luc Picard' });
doc; // { name: 'Jean-Luc Picard', _id: ObjectId('000000000000000000000000') }

const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };

// The result of `findOneAndUpdate()` is the document _before_ `update` was applied
doc = await Character.findOneAndUpdate(filter, update);
doc; // { name: 'Jean-Luc Picard', _id: ObjectId('000000000000000000000000') }

doc = await Character.findOne(filter);
doc.age; // 59

You should set the returnDocument option to 'after' to return the document after update was applied.

javascript acquit:tutorial.*findoneandupdate.*returndocument option
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };

// `doc` is the document _after_ `update` was applied because of
// `returnDocument: 'after'`
const doc = await Character.findOneAndUpdate(filter, update, {
  returnDocument: 'after'
});
doc.name; // 'Jean-Luc Picard'
doc.age; // 59

Mongoose's findOneAndUpdate() is slightly different from the MongoDB Node.js driver's findOneAndUpdate() because it returns the document itself, not a result object.

The new and returnOriginal options are deprecated in favor of returnDocument.
Use returnDocument: 'after' instead of new: true or returnOriginal: false.
Use returnDocument: 'before' instead of new: false or returnOriginal: true.

Undefined Values in Updates

Mongoose removes undefined values from updates.
For example, if you set name to undefined in the following findOneAndUpdate() call, Mongoose removes name from $set before sending the update to MongoDB.
That means name remains unchanged.
Use $unset if you want to remove a property from the document.

javascript acquit:tutorial.*findoneandupdate.*strips undefined from updates
const filter = { name: 'Jean-Luc Picard' };
const update = {
  $set: {
    name: undefined,
    age: 59
  }
};

const doc = await Character.findOneAndUpdate(filter, update, {
  returnDocument: 'after'
});
doc.name; // 'Jean-Luc Picard'
doc.age; // 59

Atomic Updates

With the exception of an unindexed upsert, findOneAndUpdate() is atomic. That means you can assume the document doesn't change between when MongoDB finds the document and when it updates the document, unless you're doing an upsert.

For example, if you're using save() to update a document, the document can change in MongoDB in between when you load the document using findOne() and when you save the document using save() as show below. For many use cases, the save() race condition is a non-issue. But you can work around it with findOneAndUpdate() (or transactions) if you need to.

javascript acquit:tutorial.*findoneandupdate.*save race condition
const filter = { name: 'Jean-Luc Picard' };
const update = { age: 59 };

let doc = await Character.findOne({ name: 'Jean-Luc Picard' });

// Document changed in MongoDB, but not in Mongoose
await Character.updateOne(filter, { name: 'Will Riker' });

// This will update `doc` age to `59`, even though the doc changed.
doc.age = update.age;
await doc.save();

doc = await Character.findOne();
doc.name; // Will Riker
doc.age; // 59

Upsert

Using the upsert option, you can use findOneAndUpdate() as a find-and-upsert operation. An upsert behaves like a normal findOneAndUpdate() if it finds a document that matches filter. But, if no document matches filter, MongoDB will insert one by combining filter and update as shown below.

javascript acquit:tutorial.*findoneandupdate.*upsert
const filter = { name: 'Will Riker' };
const update = { age: 29 };

await Character.countDocuments(filter); // 0

const doc = await Character.findOneAndUpdate(filter, update, {
  new: true,
  upsert: true // Make this update into an upsert
});
doc.name; // Will Riker
doc.age; // 29

The `includeResultMetadata` Option

Mongoose transforms the result of findOneAndUpdate() by default: it
returns the updated document. That makes it difficult to check whether
a document was upserted or not. In order to get the updated document
and check whether MongoDB upserted a new document in the same operation,
you can set the includeResultMetadata flag to make Mongoose return the raw result
from MongoDB.

javascript acquit:tutorial.*findoneandupdate.*includeresultmetadata$
const filter = { name: 'Will Riker' };
const update = { age: 29 };

await Character.countDocuments(filter); // 0

const res = await Character.findOneAndUpdate(filter, update, {
  new: true,
  upsert: true,
  // Return additional properties about the operation, not just the document
  includeResultMetadata: true
});

res.value instanceof Character; // true
// The below property will be `false` if MongoDB upserted a new
// document, and `true` if MongoDB updated an existing object.
res.lastErrorObject.updatedExisting; // false

Here's what the res object from the above example looks like:

txt
{ lastErrorObject:
   { n: 1,
     updatedExisting: false,
     upserted: 5e6a9e5ec6e44398ae2ac16a },
  value:
   { _id: 5e6a9e5ec6e44398ae2ac16a,
     name: 'Will Riker',
     __v: 0,
     age: 29 },
  ok: 1 }

Updating Discriminator Keys

Mongoose prevents updating the discriminator key using findOneAndUpdate() by default.
For example, suppose you have the following discriminator models.

javascript
const eventSchema = new mongoose.Schema({ time: Date });
const Event = db.model('Event', eventSchema);

const ClickedLinkEvent = Event.discriminator(
  'ClickedLink',
  new mongoose.Schema({ url: String })
);

const SignedUpEvent = Event.discriminator(
  'SignedUp',
  new mongoose.Schema({ username: String })
);

Mongoose will remove __t (the default discriminator key) from the update parameter, if __t is set.
This is to prevent unintentional updates to the discriminator key; for example, if you're passing untrusted user input to the update parameter.
However, you can tell Mongoose to allow updating the discriminator key by setting the overwriteDiscriminatorKey option to true as shown below.

javascript acquit:use overwritediscriminatorkey to change discriminator key
let event = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' });
await event.save();

event = await ClickedLinkEvent.findByIdAndUpdate(
  event._id,
  { __t: 'SignedUp' },
  { overwriteDiscriminatorKey: true, new: true }
);
event.__t; // 'SignedUp', updated discriminator key

File: docs/tutorials/getters-setters.md

Getters/Setters in Mongoose

Mongoose getters and setters allow you to execute custom logic when getting or setting a property on a Mongoose document. Getters let you transform data in MongoDB into a more user friendly form, and setters let you transform user data before it gets to MongoDB.

Getters

Suppose you have a User collection and you want to obfuscate user emails to protect your users' privacy. Below is a basic userSchema that obfuscates the user's email address.

javascript acquit:getters/setters.*getters.*basic example
const userSchema = new Schema({
  email: {
    type: String,
    get: obfuscate
  }
});

// Mongoose passes the raw value in MongoDB `email` to the getter
function obfuscate(email) {
  const separatorIndex = email.indexOf('@');
  if (separatorIndex < 3) {
    // '[email protected]' -> '**@gmail.com'
    return email.slice(0, separatorIndex).replace(/./g, '*') +
      email.slice(separatorIndex);
  }
  // '[email protected]' -> 'te****@gmail.com'
  return email.slice(0, 2) +
    email.slice(2, separatorIndex).replace(/./g, '*') +
    email.slice(separatorIndex);
}

const User = mongoose.model('User', userSchema);
const user = new User({ email: '[email protected]' });
user.email; // **@gmail.com

Keep in mind that getters do not impact the underlying data stored in
MongoDB. If you save user, the email property will be '[email protected]' in
the database.

By default, Mongoose does not execute getters when converting a document to JSON, including Express' res.json() function.

javascript
app.get(function(req, res) {
  return User.findOne().
    // The `email` getter will NOT run here
    then(doc => res.json(doc)).
    catch(err => res.status(500).json({ message: err.message }));
});

To run getters when converting a document to JSON, set the toJSON.getters option to true in your schema as shown below.

javascript
const userSchema = new Schema({
  email: {
    type: String,
    get: obfuscate
  }
}, { toJSON: { getters: true } });

// Or, globally
mongoose.set('toJSON', { getters: true });

// Or, on a one-off basis
app.get(function(req, res) {
  return User.findOne().
    // The `email` getter will run here
    then(doc => res.json(doc.toJSON({ getters: true }))).
    catch(err => res.status(500).json({ message: err.message }));
});

To skip getters on a one-off basis, use user.get() with the getters option set to false as shown below.

javascript acquit:getters/setters.*getters.*skip
user.get('email', null, { getters: false }); // '[email protected]'

Setters

Suppose you want to make sure all user emails in your database are lowercased to
make it easy to search without worrying about case. Below is an example
userSchema that ensures emails are lowercased.

javascript acquit:getters/setters.*setters.*basic
const userSchema = new Schema({
  email: {
    type: String,
    set: v => v.toLowerCase()
  }
});

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

const user = new User({ email: '[email protected]' });
user.email; // '[email protected]'

// The raw value of `email` is lowercased
user.get('email', null, { getters: false }); // '[email protected]'

user.set({ email: '[email protected]' });
user.email; // '[email protected]'

Mongoose also runs setters on update operations, like updateOne(). Mongoose will
upsert a document with a
lowercased email in the below example.

javascript acquit:getters/setters.*setters.*updates
await User.updateOne({}, { email: '[email protected]' }, { upsert: true });

const doc = await User.findOne();
doc.email; // '[email protected]'

In a setter function, this can be either the document being set or the query
being run. If you don't want your setter to run when you call updateOne(),
you add an if statement that checks if this is a Mongoose document as shown
below.

javascript acquit:getters/setters.*setters.*update skip
const userSchema = new Schema({
  email: {
    type: String,
    set: toLower
  }
});

function toLower(email) {
  // Don't transform `email` if using `updateOne()` or `updateMany()`
  if (!(this instanceof mongoose.Document)) {
    return email;
  }
  return email.toLowerCase();
}

const User = mongoose.model('User', userSchema);
await User.updateOne({}, { email: '[email protected]' }, { upsert: true });

const doc = await User.findOne();
doc.email; // '[email protected]'

Passing Parameters using `$locals`

You can't pass parameters to your getter and setter functions like you do to normal function calls.
To configure or pass additional properties to your getters and setters, you can use the document's $locals property.

The $locals property is the preferred place to store any program-defined data on your document without conflicting with schema-defined properties.
In your getter and setter functions, this is the document being accessed, so you set properties on $locals and then access those properties in your getters examples.
For example, the following shows how you can use $locals to configure the language for a custom getter that returns a string in different languages.

javascript acquit:getters/setters.*localization.*locale
const internationalizedStringSchema = new Schema({
  en: String,
  es: String
});

const ingredientSchema = new Schema({
  // Instead of setting `name` to just a string, set `name` to a map
  // of language codes to strings.
  name: {
    type: internationalizedStringSchema,
    // When you access `name`, pull the document's locale
    get: function(value) {
      return value[this.$locals.language || 'en'];
    }
  }
});

const recipeSchema = new Schema({
  ingredients: [{ type: mongoose.ObjectId, ref: 'Ingredient' }]
});

const Ingredient = mongoose.model('Ingredient', ingredientSchema);
const Recipe = mongoose.model('Recipe', recipeSchema);

// Create some sample data
const { _id } = await Ingredient.create({
  name: {
    en: 'Eggs',
    es: 'Huevos'
  }
});
await Recipe.create({ ingredients: [_id] });

// Populate with setting `$locals.language` for internationalization
const language = 'es';
const recipes = await Recipe.find().populate({
  path: 'ingredients',
  transform: function(doc) {
    doc.$locals.language = language;
    return doc;
  }
});

// Gets the ingredient's name in Spanish `name.es`
assert.equal(recipes[0].ingredients[0].name, 'Huevos'); // 'Huevos'

Differences vs ES6 Getters/Setters

Mongoose setters are different from ES6 setters because they allow you to transform the value being set. With ES6 setters, you
would need to store an internal _email property to use a setter. With Mongoose,
you do not need to define an internal _email property or define a
corresponding getter for email.

javascript acquit:getters/setters.*setters.*vs es6
class User {
  // This won't convert the email to lowercase! That's because `email`
  // is just a setter, the actual `email` property doesn't store any data.
  // also eslint will warn about using "return" on a setter
  set email(v) {
    // eslint-disable-next-line no-setter-return
    return v.toLowerCase();
  }
}

const user = new User();
user.email = '[email protected]';

user.email; // undefined

File: docs/tutorials/lean.md

Faster Mongoose Queries With Lean

The lean option tells Mongoose to skip
hydrating the result documents. This
makes queries faster and less memory intensive, but the result documents are
plain old JavaScript objects (POJOs), not Mongoose documents.
In this tutorial, you'll learn more about the tradeoffs of using lean().

Using Lean

By default, Mongoose queries return an instance of the
Mongoose Document class. Documents are much
heavier than vanilla JavaScript objects, because they have a lot of internal
state for change tracking. Enabling the lean option tells Mongoose to skip
instantiating a full Mongoose document and just give you the POJO.

javascript
const leanDoc = await MyModel.findOne().lean();

How much smaller are lean documents? Here's a comparison.

javascript acquit:lean tutorial.*compare sizes
const schema = new mongoose.Schema({ name: String });
const MyModel = mongoose.model('Test', schema);

await MyModel.create({ name: 'test' });

const normalDoc = await MyModel.findOne();
// To enable the `lean` option for a query, use the `lean()` function.
const leanDoc = await MyModel.findOne().lean();

v8Serialize(normalDoc).length; // approximately 180
v8Serialize(leanDoc).length; // approximately 55, about 3x smaller!

// In case you were wondering, the JSON form of a Mongoose doc is the same
// as the POJO. This additional memory only affects how much memory your
// Node.js process uses, not how much data is sent over the network.
JSON.stringify(normalDoc).length === JSON.stringify(leanDoc).length; // true

Under the hood, after executing a query, Mongoose converts the query results
from POJOs to Mongoose documents. If you turn on the lean option, Mongoose
skips this step.

javascript acquit:lean tutorial.*compare types
const normalDoc = await MyModel.findOne();
const leanDoc = await MyModel.findOne().lean();

normalDoc instanceof mongoose.Document; // true
normalDoc.constructor.name; // 'model'

leanDoc instanceof mongoose.Document; // false
leanDoc.constructor.name; // 'Object'

The downside of enabling lean is that lean docs don't have:

  • Change tracking
  • Casting and validation
  • Getters and setters
  • Virtuals
  • save()

For example, the following code sample shows that the Person model's getters
and virtuals don't run if you enable lean.

javascript acquit:lean tutorial.*getters and virtuals
// Define a `Person` model. Schema has 2 custom getters and a `fullName`
// virtual. Neither the getters nor the virtuals will run if lean is enabled.
const personSchema = new mongoose.Schema({
  firstName: {
    type: String,
    get: capitalizeFirstLetter
  },
  lastName: {
    type: String,
    get: capitalizeFirstLetter
  }
});
personSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});
function capitalizeFirstLetter(v) {
  // Convert 'bob' -> 'Bob'
  return v.charAt(0).toUpperCase() + v.substring(1);
}
const Person = mongoose.model('Person', personSchema);

// Create a doc and load it as a lean doc
await Person.create({ firstName: 'benjamin', lastName: 'sisko' });
const normalDoc = await Person.findOne();
const leanDoc = await Person.findOne().lean();

normalDoc.fullName; // 'Benjamin Sisko'
normalDoc.firstName; // 'Benjamin', because of `capitalizeFirstLetter()`
normalDoc.lastName; // 'Sisko', because of `capitalizeFirstLetter()`

leanDoc.fullName; // undefined
leanDoc.firstName; // 'benjamin', custom getter doesn't run
leanDoc.lastName; // 'sisko', custom getter doesn't run

Lean and Populate

Populate works with lean(). If you
use both populate() and lean(), the lean option propagates to the
populated documents as well. In the below example, both the top-level
'Group' documents and the populated 'Person' documents will be lean.

javascript acquit:lean tutorial.*conventional populate
// Create models
const Group = mongoose.model('Group', new mongoose.Schema({
  name: String,
  members: [{ type: mongoose.ObjectId, ref: 'Person' }]
}));
const Person = mongoose.model('Person', new mongoose.Schema({
  name: String
}));

// Initialize data
const people = await Person.create([
  { name: 'Benjamin Sisko' },
  { name: 'Kira Nerys' }
]);
await Group.create({
  name: 'Star Trek: Deep Space Nine Characters',
  members: people.map(p => p._id)
});

// Execute a lean query
const group = await Group.findOne().lean().populate('members');
group.members[0].name; // 'Benjamin Sisko'
group.members[1].name; // 'Kira Nerys'

// Both the `group` and the populated `members` are lean.
group instanceof mongoose.Document; // false
group.members[0] instanceof mongoose.Document; // false
group.members[1] instanceof mongoose.Document; // false

Virtual populate also works with lean.

javascript acquit:lean tutorial.*virtual populate
// Create models
const groupSchema = new mongoose.Schema({ name: String });
groupSchema.virtual('members', {
  ref: 'Person',
  localField: '_id',
  foreignField: 'groupId'
});
const Group = mongoose.model('Group', groupSchema);
const Person = mongoose.model('Person', new mongoose.Schema({
  name: String,
  groupId: mongoose.ObjectId
}));

// Initialize data
const g = await Group.create({ name: 'DS9 Characters' });
await Person.create([
  { name: 'Benjamin Sisko', groupId: g._id },
  { name: 'Kira Nerys', groupId: g._id }
]);

// Execute a lean query
const group = await Group.findOne().lean().populate({
  path: 'members',
  options: { sort: { name: 1 } }
});
group.members[0].name; // 'Benjamin Sisko'
group.members[1].name; // 'Kira Nerys'

// Both the `group` and the populated `members` are lean.
group instanceof mongoose.Document; // false
group.members[0] instanceof mongoose.Document; // false
group.members[1] instanceof mongoose.Document; // false

When to Use Lean

If you're executing a query and sending the results without modification to,
say, an Express response, you should
use lean. In general, if you do not modify the query results and do not use
custom getters, you should use
lean(). If you modify the query results or rely on features like getters
or transforms, you should not
use lean().

Below is an example of an Express route
that is a good candidate for lean(). This route does not modify the person
doc and doesn't rely on any Mongoose-specific functionality.

javascript
// As long as you don't need any of the Person model's virtuals or getters,
// you can use `lean()`.
app.get('/person/:id', function(req, res) {
  Person.findOne({ _id: req.params.id }).lean().
    then(person => res.json({ person })).
    catch(error => res.json({ error: error.message }));
});

Below is an example of an Express route that should not use lean(). As
a general rule of thumb, GET routes are good candidates for lean() in a
RESTful API.
On the other hand, PUT, POST, etc. routes generally should not use lean().

javascript
// This route should **not** use `lean()`, because lean means no `save()`.
app.put('/person/:id', function(req, res) {
  Person.findOne({ _id: req.params.id }).
    then(person => {
      assert.ok(person);
      Object.assign(person, req.body);
      return person.save();
    }).
    then(person => res.json({ person })).
    catch(error => res.json({ error: error.message }));
});

Remember that virtuals do not end up in lean() query results. Use the
mongoose-lean-virtuals plugin
to add virtuals to your lean query results.

Plugins

Using lean() bypasses all Mongoose features, including virtuals, getters/setters,
and defaults. If you want to
use these features with lean(), you need to use the corresponding plugin:

However, you need to keep in mind that Mongoose does not hydrate lean documents,
so this will be a POJO in virtuals, getters, and default functions.

javascript
const schema = new Schema({ name: String });
schema.plugin(require('mongoose-lean-virtuals'));

schema.virtual('lowercase', function() {
  this instanceof mongoose.Document; // false

  this.name; // Works
  this.get('name'); // Crashes because `this` is not a Mongoose document.
});

BigInts

By default, the MongoDB Node driver converts longs stored in MongoDB into JavaScript numbers, not BigInts.
Set the useBigInt64 option on your lean() queries to inflate longs into BigInts.

javascript acquit:lean tutorial.*bigint
const Person = mongoose.model('Person', new mongoose.Schema({
  name: String,
  age: BigInt
}));
// Mongoose will convert `age` to a BigInt
const { age } = await Person.create({ name: 'Benjamin Sisko', age: 37 });
typeof age; // 'bigint'

// By default, if you store a document with a BigInt property in MongoDB and you
// load the document with `lean()`, the BigInt property will be a number
let person = await Person.findOne({ name: 'Benjamin Sisko' }).lean();
typeof person.age; // 'number'

// Set the `useBigInt64` option to opt in to converting MongoDB longs to BigInts.
person = await Person.findOne({ name: 'Benjamin Sisko' }).
  setOptions({ useBigInt64: true }).
  lean();
typeof person.age; // 'bigint'

File: docs/tutorials/query_casting.md

Query Casting

The first parameter to Model.find(), Query#find(), Model.findOne(), etc. is called filter.
In older content this parameter is sometimes called query or conditions. For example:

javascript acquit:cast tutorial.*get and set
const query = Character.find({ name: 'Jean-Luc Picard' });
query.getFilter(); // `{ name: 'Jean-Luc Picard' }`

// Subsequent chained calls merge new properties into the filter
query.find({ age: { $gt: 50 } });
query.getFilter(); // `{ name: 'Jean-Luc Picard', age: { $gt: 50 } }`

When you execute the query using Query#exec() or Query#then(), Mongoose casts the filter to match your schema.

javascript acquit:cast tutorial.*cast values
// Note that `_id` and `age` are strings. Mongoose will cast `_id` to
// a MongoDB ObjectId and `age.$gt` to a number.
const query = Character.findOne({
  _id: '5cdc267dd56b5662b7b7cc0c',
  age: { $gt: '50' }
});

// `{ _id: '5cdc267dd56b5662b7b7cc0c', age: { $gt: '50' } }`
// Query hasn't been executed yet, so Mongoose hasn't casted the filter.
query.getFilter();

const doc = await query.exec();
doc.name; // "Jean-Luc Picard"

// Mongoose casted the filter, so `_id` became an ObjectId and `age.$gt`
// became a number.
query.getFilter()._id instanceof mongoose.Types.ObjectId; // true
typeof query.getFilter().age.$gt === 'number'; // true

If Mongoose fails to cast the filter to your schema, your query will throw a CastError.

javascript acquit:cast tutorial.*cast error
const query = Character.findOne({ age: { $lt: 'not a number' } });

const err = await query.exec().then(() => null, err => err);
err instanceof mongoose.CastError; // true
// Cast to number failed for value "not a number" at path "age" for
// model "Character"
err.message;

The `strictQuery` Option

By default, Mongoose does not cast filter properties that aren't in your schema.

javascript acquit:cast tutorial.*not in schema
const query = Character.findOne({ notInSchema: { $lt: 'not a number' } });

// No error because `notInSchema` is not defined in the schema
await query.exec();

You can configure this behavior using the strictQuery option for schemas. This option is analogous to the strict option. Setting strictQuery to true removes non-schema properties from the filter:

javascript acquit:cast tutorial.*strictquery true
mongoose.deleteModel('Character');
const schema = new mongoose.Schema({ name: String, age: Number }, {
  strictQuery: true
});
Character = mongoose.model('Character', schema);

const query = Character.findOne({ notInSchema: { $lt: 'not a number' } });

await query.exec();
query.getFilter(); // Empty object `{}`, Mongoose removes `notInSchema`

To make Mongoose throw an error if your filter has a property that isn't in the schema, set strictQuery to 'throw':

javascript acquit:cast tutorial.*strictquery throw
mongoose.deleteModel('Character');
const schema = new mongoose.Schema({ name: String, age: Number }, {
  strictQuery: 'throw'
});
Character = mongoose.model('Character', schema);

const query = Character.findOne({ notInSchema: { $lt: 'not a number' } });

const err = await query.exec().then(() => null, err => err);
err.name; // 'StrictModeError'
// Path "notInSchema" is not in schema and strictQuery is 'throw'.
err.message;

Implicit `$in`

Because of schemas, Mongoose knows what types fields should be, so it can provide some neat syntactic sugar. For example, if you forget to put $in on a non-array field, Mongoose will add $in for you.

javascript acquit:cast tutorial.*implicit in
// Normally wouldn't find anything because `name` is a string, but
// Mongoose automatically inserts `$in`
const query = Character.findOne({ name: ['Jean-Luc Picard', 'Will Riker'] });

const doc = await query.exec();
doc.name; // "Jean-Luc Picard"

// `{ name: { $in: ['Jean-Luc Picard', 'Will Riker'] } }`
query.getFilter();

File: docs/tutorials/ssl.md

TLS/SSL Connections

Mongoose supports connecting to MongoDB clusters that require TLS/SSL connections. Setting the tls option to true in mongoose.connect() or your connection string is enough to connect to a MongoDB cluster using TLS/SSL:

javascript
mongoose.connect('mongodb://127.0.0.1:27017/test', { tls: true });

// Equivalent:
mongoose.connect('mongodb://127.0.0.1:27017/test?tls=true');

The tls option defaults to false for connection strings that start with mongodb://. However,
the tls option defaults to true for connection strings that start with mongodb+srv://. So if you are using an srv connection string to connect to MongoDB Atlas, TLS/SSL is enabled by default.

If you try to connect to a MongoDB cluster that requires TLS/SSL without enabling the tls/ssl option, mongoose.connect() will error out with the below error:

no-highlight
MongooseServerSelectionError: connection <monitor> to 127.0.0.1:27017 closed
    at NativeConnection.Connection.openUri (/node_modules/mongoose/lib/connection.js:800:32)
    ...

TLS/SSL Validation

By default, Mongoose validates the TLS/SSL certificate against a certificate authority to ensure the TLS/SSL certificate is valid. To disable this validation, set the tlsAllowInvalidCertificates (or tlsInsecure) option to true.

javascript
mongoose.connect('mongodb://127.0.0.1:27017/test', {
  tls: true,
  tlsAllowInvalidCertificates: true,
});

In most cases, you should not disable TLS/SSL validation in production. However, tlsAllowInvalidCertificates: true is often helpful
for debugging SSL connection issues. If you can connect to MongoDB with tlsAllowInvalidCertificates: true, but not with
tlsAllowInvalidCertificates: false, then you can confirm Mongoose can connect to the server and the server is configured to use
TLS/SSL correctly, but there's some issue with the certificate.

For example, a common issue is the below error message:

no-highlight
MongooseServerSelectionError: unable to verify the first certificate

This error is often caused by self-signed MongoDB certificates or other situations where the certificate sent by the MongoDB
server is not registered with an established certificate authority. The solution is to set the tlsCAFile option, which essentially sets a list of allowed SSL certificates.

javascript
await mongoose.connect('mongodb://127.0.0.1:27017/test', {
  tls: true,
  // For example, see https://medium.com/@rajanmaharjan/secure-your-mongodb-connections-ssl-tls-92e2addb3c89
  // for where the `rootCA.pem` file comes from.
  tlsCAFile: `${__dirname}/rootCA.pem`,
});

Another common issue is the below error message:

no-highlight
MongooseServerSelectionError: Hostname/IP does not match certificate's altnames: Host: hostname1. is not cert's CN: hostname2

The SSL certificate's common name must line up with the host name
in your connection string. If the SSL certificate is for hostname2.mydomain.com, your connection string must connect to hostname2.mydomain.com, not any other hostname or IP address that may be equivalent to hostname2.mydomain.com. For replica sets, this also means that the SSL certificate's common name must line up with the machine's hostname. To disable this validation, set the tlsAllowInvalidHostnames option to true.

X.509 Authentication

If you're using X.509 authentication, you should set the user name in the connection string, not the connect() options.

javascript
// Do this:
const username = 'myusername';
await mongoose.connect(`mongodb://${encodeURIComponent(username)}@127.0.0.1:27017/test`, {
  tls: true,
  tlsCAFile: `${__dirname}/rootCA.pem`,
  authMechanism: 'MONGODB-X509',
});

// Not this:
await mongoose.connect('mongodb://127.0.0.1:27017/test', {
  tls: true,
  tlsCAFile: `${__dirname}/rootCA.pem`,
  authMechanism: 'MONGODB-X509',
  auth: { username },
});

X.509 Authentication with MongoDB Atlas

With MongoDB Atlas, X.509 certificates are not Root CA certificates and will not work with the tlsCAFile parameter as self-signed certificates would. If the tlsCAFile parameter is used an error similar to the following would be raised:

no-highlight
MongoServerSelectionError: unable to get local issuer certificate

To connect to a MongoDB Atlas cluster using X.509 authentication the correct option to set is tlsCertificateKeyFile. The connection string already specifies the authSource and authMechanism, however they're included below as connect() options for completeness:

javascript
const url = 'mongodb+srv://xyz.mongodb.net/test?authSource=%24external&authMechanism=MONGODB-X509';
await mongoose.connect(url, {
  tls: true,
  // location of a local .pem file that contains both the client's certificate and key
  tlsCertificateKeyFile: '/path/to/certificate.pem',
  authMechanism: 'MONGODB-X509',
  authSource: '$external',
});

Note The connection string options must be URL escaped correctly.


File: docs/tutorials/virtuals.md

Mongoose Virtuals

In Mongoose, a virtual is a property that is not stored in MongoDB.
Virtuals are typically used for computed properties on documents.

Your First Virtual

Suppose you have a User model. Every user has an email, but you also
want the email's domain. For example, the domain portion of
'[email protected]' is 'gmail.com'.

Below is one way to implement the domain property using a virtual.
You define virtuals on a schema using the Schema#virtual() function.

javascript acquit:virtuals.*basic
const userSchema = mongoose.Schema({
  email: String
});
// Create a virtual property `domain` that's computed from `email`.
userSchema.virtual('domain').get(function() {
  return this.email.slice(this.email.indexOf('@') + 1);
});
const User = mongoose.model('User', userSchema);

const doc = await User.create({ email: '[email protected]' });
// `domain` is now a property on User documents.
doc.domain; // 'gmail.com'

The Schema#virtual() function returns a VirtualType object. Unlike normal document properties,
virtuals do not have any underlying value and Mongoose does not do
any type coercion on virtuals. However, virtuals do have
getters and setters, which make
them ideal for computed properties, like the domain example above.

Virtual Setters

You can also use virtuals to set multiple properties at once as an
alternative to custom setters on normal properties. For example, suppose
you have two string properties: firstName and lastName. You can
create a virtual property fullName that lets you set both of
these properties at once. The key detail is that, in virtual getters and
setters, this refers to the document the virtual is attached to.

javascript acquit:virtuals.*fullname
const userSchema = mongoose.Schema({
  firstName: String,
  lastName: String
});
// Create a virtual property `fullName` with a getter and setter.
userSchema.virtual('fullName').
  get(function() { return `${this.firstName} ${this.lastName}`; }).
  set(function(v) {
    // `v` is the value being set, so use the value to set
    // `firstName` and `lastName`.
    const firstName = v.substring(0, v.indexOf(' '));
    const lastName = v.substring(v.indexOf(' ') + 1);
    this.set({ firstName, lastName });
  });
const User = mongoose.model('User', userSchema);

const doc = new User();
// Vanilla JavaScript assignment triggers the setter
doc.fullName = 'Jean-Luc Picard';

doc.fullName; // 'Jean-Luc Picard'
doc.firstName; // 'Jean-Luc'
doc.lastName; // 'Picard'

Virtuals in JSON

By default, Mongoose does not include virtuals when you convert a document to JSON.
For example, if you pass a document to Express' res.json() function, virtuals will not be included by default.

To include virtuals in res.json(), you need to set the
toJSON schema option to { virtuals: true }.

javascript acquit:virtuals.*tojson
const opts = { toJSON: { virtuals: true } };
const userSchema = mongoose.Schema({
  _id: Number,
  email: String
}, opts);
// Create a virtual property `domain` that's computed from `email`.
userSchema.virtual('domain').get(function() {
  return this.email.slice(this.email.indexOf('@') + 1);
});
const User = mongoose.model('User', userSchema);

const doc = new User({ _id: 1, email: '[email protected]' });

doc.toJSON().domain; // 'gmail.com'
// {"_id":1,"email":"[email protected]","domain":"gmail.com","id":"1"}
JSON.stringify(doc);

// To skip applying virtuals, pass `virtuals: false` to `toJSON()`
doc.toJSON({ virtuals: false }).domain; // undefined

Virtuals in `console.log()`

By default, Mongoose does not include virtuals in console.log() output.
To include virtuals in console.log(), you need to set the toObject schema option to { virtuals: true }, or use toObject() before printing the object.

javascript
console.log(doc.toObject({ virtuals: true }));

Virtuals with Lean

Virtuals are properties on Mongoose documents. If you use the
lean option, that means your queries return POJOs
rather than full Mongoose documents. That means no virtuals if you use
lean().

javascript acquit:virtuals.*lean
const fullDoc = await User.findOne();
fullDoc.domain; // 'gmail.com'

const leanDoc = await User.findOne().lean();
leanDoc.domain; // undefined

If you use lean() for performance, but still need virtuals, Mongoose
has an
officially supported mongoose-lean-virtuals plugin
that decorates lean documents with virtuals.

Limitations

Mongoose virtuals are not stored in MongoDB, which means you can't query
based on Mongoose virtuals.

javascript acquit:virtuals.*in query
// Will **not** find any results, because `domain` is not stored in
// MongoDB.
const doc = await User.findOne({ domain: 'gmail.com' }, null, { strictQuery: false });
doc; // undefined

If you want to query by a computed property, you should set the property using
a custom setter or pre save middleware.

Populate

Mongoose also supports populating virtuals. A populated
virtual contains documents from another collection. To define a populated
virtual, you need to specify:

  • The ref option, which tells Mongoose which model to populate documents from.
  • The localField and foreignField options. Mongoose will populate documents from the model in ref whose foreignField matches this document's localField.
javascript acquit:virtuals.*populate
const userSchema = mongoose.Schema({ _id: Number, email: String });
const blogPostSchema = mongoose.Schema({
  title: String,
  authorId: Number
});
// When you `populate()` the `author` virtual, Mongoose will find the
// first document in the User model whose `_id` matches this document's
// `authorId` property.
blogPostSchema.virtual('author', {
  ref: 'User',
  localField: 'authorId',
  foreignField: '_id',
  justOne: true
});
const User = mongoose.model('User', userSchema);
const BlogPost = mongoose.model('BlogPost', blogPostSchema);

await BlogPost.create({ title: 'Introduction to Mongoose', authorId: 1 });
await User.create({ _id: 1, email: '[email protected]' });

const doc = await BlogPost.findOne().populate('author');
doc.author.email; // '[email protected]'

Virtuals via schema options

Virtuals can also be defined in the schema-options directly without having to use .virtual:

javascript acquit:virtuals.*schema-options fullname
const userSchema = mongoose.Schema({
  firstName: String,
  lastName: String
}, {
  virtuals: {
    // Create a virtual property `fullName` with a getter and setter
    fullName: {
      get() { return `${this.firstName} ${this.lastName}`; },
      set(v) {
        // `v` is the value being set, so use the value to set
        // `firstName` and `lastName`.
        const firstName = v.substring(0, v.indexOf(' '));
        const lastName = v.substring(v.indexOf(' ') + 1);
        this.set({ firstName, lastName });
      }
    }
  }
});
const User = mongoose.model('User', userSchema);

const doc = new User();
// Vanilla JavaScript assignment triggers the setter
doc.fullName = 'Jean-Luc Picard';

doc.fullName; // 'Jean-Luc Picard'
doc.firstName; // 'Jean-Luc'
doc.lastName; // 'Picard'

The same also goes for virtual options, like virtual populate:

javascript acquit:virtuals.*schema-options populate
const userSchema = mongoose.Schema({ _id: Number, email: String });
const blogPostSchema = mongoose.Schema({
  title: String,
  authorId: Number
}, {
  virtuals: {
    // When you `populate()` the `author` virtual, Mongoose will find the
    // first document in the User model whose `_id` matches this document's
    // `authorId` property.
    author: {
      options: {
        ref: 'User',
        localField: 'authorId',
        foreignField: '_id',
        justOne: true
      }
    }
  }
});
const User = mongoose.model('User', userSchema);
const BlogPost = mongoose.model('BlogPost', blogPostSchema);

await BlogPost.create({ title: 'Introduction to Mongoose', authorId: 1 });
await User.create({ _id: 1, email: '[email protected]' });

const doc = await BlogPost.findOne().populate('author');
doc.author.email; // '[email protected]'

Further Reading


File: docs/typescript/populate.md

Populate with TypeScript

Mongoose's TypeScript bindings add a generic parameter Paths to the populate():

typescript
import { Schema, model, Document, Types } from 'mongoose';

// `Parent` represents the object as it is stored in MongoDB
interface Parent {
  child?: Types.ObjectId,
  name?: string
}
const ParentModel = model('Parent', new Schema({
  child: { type: Schema.Types.ObjectId, ref: 'Child' },
  name: String
}));

interface Child {
  name: string;
}
const childSchema = new Schema({ name: String });
const ChildModel = model<Child>('Child', childSchema);

// Populate with `Paths` generic `{ child: Child }` to override `child` path
ParentModel.findOne({}).populate<{ child: Child }>('child').orFail().then(doc => {
  // Works
  const t: string = doc.child.name;
});

An alternative approach is to define a PopulatedParent interface and use Pick<> to pull the properties you're populating.

ts
import { Schema, model, Document, Types } from 'mongoose';

// `Parent` represents the object as it is stored in MongoDB
interface Parent {
  child?: Types.ObjectId,
  name?: string
}
interface Child {
  name: string;
}
interface PopulatedParent {
  child: Child | null;
}
const ParentModel = model('Parent', new Schema({
  child: { type: Schema.Types.ObjectId, ref: 'Child' },
  name: String
}));
const childSchema = new Schema({ name: String });
const ChildModel = model<Child>('Child', childSchema);

// Populate with `Paths` generic `{ child: Child }` to override `child` path
ParentModel.findOne({}).populate>('child').orFail().then(doc => {
  // Works
  const t: string = doc.child.name;
});

Using `PopulatedDoc`

Mongoose also exports a PopulatedDoc type that helps you define populated documents in your document interface:

ts
import { Schema, model, Document, PopulatedDoc } from 'mongoose';

// `child` is either an ObjectId or a populated document
interface Parent {
  child?: PopulatedDoc<Document<ObjectId> & Child>,
  name?: string
}
const ParentModel = model('Parent', new Schema({
  child: { type: 'ObjectId', ref: 'Child' },
  name: String
}));

interface Child {
  name?: string;
}
const childSchema = new Schema({ name: String });
const ChildModel = model<Child>('Child', childSchema);

ParentModel.findOne({}).populate('child').orFail().then((doc: Parent) => {
  const child = doc.child;
  if (child == null || child instanceof ObjectId) {
    throw new Error('should be populated');
  } else {
    // Works
    doc.child.name.trim();
  }
});

However, we recommend using the .populate<{ child: Child }> syntax from the first section instead of PopulatedDoc.
Here's two reasons why:

  1. You still need to add an extra check to check if child instanceof ObjectId. Otherwise, the TypeScript compiler will fail with Property name does not exist on type ObjectId. So using PopulatedDoc<> means you need an extra check everywhere you use doc.child.
  2. In the Parent interface, child is a hydrated document, which makes it difficult for Mongoose to infer the type of child when you use lean() or toObject().