pre()
and post()
62ref
Property 78populate()
Syntax 80explain()
115Mongoose is an object-document mapping (ODM) framework for Node.js and MongoDB. It is the most popular database framework for Node.js in terms of npm downloads (over 800,000 per month) and usage on GitHub (over 939,000 repos depend on Mongoose).
I'm Valeri Karpov, the maintainer of Mongoose since April 2014. Mongoose was one of the first Node.js projects when it was written in 2010, and by 2014 the early maintainers had moved on. I had been using Mongoose since 2012, so I jumped on the opportunity to work on the project as fast as I could. I've been primarily responsible for Mongoose for most of its 10 years of existence.
This eBook contains the distilled experience of 8 years working with MongoDB and Mongoose, and 6 years working on Mongoose. The goal of this book is to take you from someone who is familiar with Node.js and MongoDB, to someone who can architect a Mongoose-based backend for a large scale business. Here are the guiding principles for this eBook:
Before we get started, here's some technical details.
#
symbol as a convenient shorthand for .prototype.
. For example, Document#save()
refers to the save()
method on instances of the Document
class.Mongoose is a powerful tool for building backend applications. Mongoose provides change tracking and schema validation out of the box, letting you work with a schema-less database without worrying about malformed data. Mongoose also provides tools for customizing your database interactions, like middleware and plugins. Mongoose's community also supports a wide variety of plugins for everything from pagination to encryption to custom types.
Are you ready to become a Mongoose master? Let's get started!
Mongoose is a object-document mapping (ODM) framework for Node.js and MongoDB. This definition is nuanced, so here's an overview of the core benefits of using Mongoose.
Mongoose is built on top of the official MongoDB Node.js Driver, which this book will refer to as just "the driver". The driver is responsible for maintaining a connection to MongoDB and serializing commands into MongoDB's wire protocol. The driver is an rapidly growing project and it is entirely possible to build an application without Mongoose. The major benefits of using Mongoose over using the driver directly are:
save()
, you can put a single log statement in pre('save')
.In order to clarify what Mongoose is, let's take a look at how Mongoose differs from other modules you may have used.
Mongoose is an ODM, not an ORM (object-relational mapping). Some common ORMs are ActiveRecord, Hibernate, and Sequelize. Here's the key difference between ORMs and ODMs: ORMs store data in relational databases, so data from one ORM object may end up in multiple rows in multiple tables. In other words, how your data looks in JavaScript may be completely different from how your ORM stores the data.
MongoDB allows storing arbitrary objects, so Mongoose doesn't need to slice your data into different collections. Like an ORM, Mongoose validates your data, provides a middleware framework, and transforms vanilla JavaScript operations into database operations. But unlike an ORM, Mongoose ensures your objects have the same structure in Node.js as they do when they're stored in MongoDB.
Mongoose is more a framework than a library, although it has elements of both. For the purposes of this book, a framework is a module that executes your code for you, whereas a library is a module that your code calls. Modules that are either a pure framework or a pure library are rare. But, for example, Lodash is a library, Mocha is a framework, and Mongoose is a bit of both.
A database driver, like the MongoDB Node.js driver, is a library. It exposes several functions that you can call, but it doesn't provide middleware or any other paradigm for structuring code that uses the library. On the other hand, Mongoose provides middleware, custom validators, custom types, and other paradigms for code organization.
The MongoDB driver doesn't prescribe any specific architecture. You can build your project using traditional MVC architecture, aspect-oriented, reactive extensions, or anything else. Mongoose, on the other hand, is designed to fit the "model" portion of MVC or MVVM.
Mongoose is an ODM for Node.js and MongoDB. It provides schema validation, change tracking, middleware, and plugins. Mongoose also makes it easy to build apps using MVC patterns. Next, let's take a look at some basic Mongoose patterns that almost all Mongoose apps share.
To use Mongoose, you need to open at least one connection to a MongoDB server, replica set, or sharded cluster.
This book will assume you already have a MongoDB instance running. If you don't have MongoDB set up yet, the easiest way to get started is a cloud instance using MongoDB Atlas' free tier. If you prefer a local MongoDB instance, run-rs is an npm module that automatically installs and runs MongoDB for you.
Here's how you connect Mongoose to a MongoDB instance running on your local machine on the default port.
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/mydb');
The first parameter to mongoose.connect()
is known as the connection string.
The connection string defines which MongoDB server(s) you're connecting to and
the name of the database you want to use. More sophisticated connection strings
can also include authentication information and configuration options.
The mydb
section of the connection string tells Mongoose which database to
use. MongoDB stores data in the form of documents. A document is essentially
a Node.js object, and analagous to a row in SQL databases. Every document
belongs to a collection, and every collection belongs to a database. For the
purposes of this book, you can think of a database as a set of collections.
Although connecting to MongoDB is an asynchronous operation, you don't have to
wait for connecting to succeed before using Mongoose. You can, and generally
should, await
on mongoose.connect()
to ensure Mongoose connects successfully.
await mongoose.connect('mongodb://localhost:27017/mydb');
However, many Mongoose apps do not wait for Mongoose to connect because it
isn't strictly necessary. Don't be surprised if you see mongoose.connect()
with no await
or then()
.
To store and query MongoDB documents using Mongoose, you need to define a model. Mongoose models are the primary tool you'll use for creating and loading documents. To define a model, you first need to define a schema.
In general, Mongoose schemas are objects that configure models. In particular, schemas are responsible for defining what properties your documents can have and what types those properties must be.
For example, suppose you're creating a model to represent a product. A minimal
product should have a string property name
and a number property price
as
shown below.
const productSchema = new mongoose.Schema({
// A product has two properties: `name` and `price`
name: String,
price: Number
});
// The `mongoose.model()` function has 2 required parameters:
// The 1st param is the model's name, a string
// The 2nd param is the schema
const Product = mongoose.model('Product', productSchema);
const product = new Product({
name: 'iPhone',
price: '800', // Note that this is a string, not a number
notInSchema: 'foo'
});
product.name; // 'iPhone'
product.price; // 800, Mongoose converted this to a number
// undefined, Mongoose removes props that aren't in the schema
product.notInSchema;
The mongoose.model()
function takes the model's name and schema as parameters,
and returns a class. That class is configured to cast, validate, and track
changes on the paths defined in the schema. Schemas have numerous features
beyond just type checking. For example, you can make Mongoose lowercase the
product's name as shown below.
const productSchema = new mongoose.Schema({
// The below means the `name` property should be a string
// with an option `lowercase` set to `true`.
name: { type: String, lowercase: true },
price: Number
});
const Product = mongoose.model('Product', productSchema);
const product = new Product({ name: 'iPhone', price: 800 });
product.name; // 'iphone', lowercased
Internally, when you instantiate
a Mongoose model, Mongoose defines
native JavaScript getters and setters
for every path in your schema on every instance of that model. Here's an
example of a simplified Product
class implemented using ES6 classes
that mimics how a Mongoose model works.
class Product {
constructor(obj) {
// `_doc` stores the raw data, bypassing getters and setters
// Otherwise `doc.name = 'foo'` causes infinite recursion
this._doc = {};
Object.assign(this, { name: obj.name, price: obj.price });
}
get name() { return this._doc.name; }
set name(v) { this._doc.name = v == null ? v : '' + v; }
get price() { return this._doc.price; }
set price(v) { this._doc.price = v == null ? v : +v; }
}
const p = new Product({ name: 'iPhone', price: '800', foo: 'bar' });
p.name; // 'iPhone'
p.price; // 800
p.foo; // undefined
In Mongoose, a model is a class, and an instance of a model is called a document. This book also defined a "document" as an object stored in a MongoDB collection in section 1.2. These two definitions are equivalent for practical purposes, because a Mongoose document in Node.js maps one-to-one to a document stored on the MongoDB server.
There are two ways to create a Mongoose document: you can instantiate a model to create a new document, or you can execute a query to load an existing document from the MongoDB server.
To create a new document, you can use your model as a constructor. This creates
a new document that has not been stored in MongoDB yet. Documents
have a save()
function that you use to persist changes to MongoDB. When you
create a new document, the save()
function sends an insertOne()
operation
to the MongoDB server.
// `doc` is just in Node.js, you haven't persisted it to MongoDB yet
const doc = new Product({ name: 'iPhone', price: 800 });
// Store `doc` in MongoDB
await doc.save();
That's how you create a new document. To load an existing document from MongoDB,
you can use the Model.find()
or Model.findOne()
functions. There are several
other query functions you can use that you'll learn about in Chapter 3.
// `Product.findOne()` returns a Mongoose _query_. Queries are
// thenable, which means you can `await` on them.
let product = await Product.findOne();
product.name; // "iPhone"
product.price; // 800
const products = await Product.find();
product = products[0];
product.name; // "iPhone"
product.price; // 800
Connecting to MongoDB, defining a schema, creating a model, creating documents, and loading documents are the basic patterns that you'll see in almost every Mongoose app. Now that you've seen the verbs describing what actions you can do with Mongoose, let's define the nouns that are responsible for these actions.