Getting Started With MongoDB & Mongoose
Rate this quickstart
In this article, we’ll learn how Mongoose, a third-party library for MongoDB, can help you to structure and access your data with ease.
Many who learn MongoDB get introduced to it through the very popular library, Mongoose. Mongoose is described as “elegant MongoDB object modeling for Node.js.”
Mongoose is an ODM (Object Data Modeling) library for MongoDB. While you don’t need to use an Object Data Modeling (ODM) or Object Relational Mapping (ORM) tool to have a great experience with MongoDB, some developers prefer them. Many Node.js developers choose to work with Mongoose to help with data modeling, schema enforcement, model validation, and general data manipulation. And Mongoose makes these tasks effortless.
If you want to hear from the maintainer of Mongoose, Val Karpov, give this episode of the MongoDB Podcast a listen!
By default, MongoDB has a flexible data model. This makes MongoDB databases very easy to alter and update in the future. But a lot of developers are accustomed to having rigid schemas.
Mongoose forces a semi-rigid schema from the beginning. With Mongoose, developers must define a Schema and Model.
A schema defines the structure of your collection documents. A Mongoose schema maps directly to a MongoDB collection.
1 const blog = new Schema({ 2 title: String, 3 slug: String, 4 published: Boolean, 5 author: String, 6 content: String, 7 tags: [String], 8 createdAt: Date, 9 updatedAt: Date, 10 comments: [{ 11 user: String, 12 content: String, 13 votes: Number 14 }] 15 });
With schemas, we define each field and its data type. Permitted types are:
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- ObjectId
- Array
- Decimal128
- Map
Models take your schema and apply it to each document in its collection.
Models are responsible for all document interactions like creating, reading, updating, and deleting (CRUD).
An important note: the first argument passed to the model should be the singular form of your collection name. Mongoose automatically changes this to the plural form, transforms it to lowercase, and uses that for the database collection name.
1 const Blog = mongoose.model('Blog', blog);
In this example,
Blog
translates to the blogs
collection.We’ll run the following commands from the terminal to get going:
1 mkdir mongodb-mongoose 2 cd mongodb-mongoose 3 npm init -y 4 npm i mongoose 5 npm i -D nodemon 6 code .
This will create the project directory, initialize, install the packages we need, and open the project in VS Code.
Let’s add a script to our
package.json
file to run our project. We will also use ES Modules instead of Common JS, so we’ll add the module type
as well. This will also allow us to use top-level await
.1 ... 2 "scripts": { 3 "dev": "nodemon index.js" 4 }, 5 "type": "module", 6 ...
Now we’ll create the
index.js
file and use Mongoose to connect to MongoDB.1 import mongoose from 'mongoose' 2 3 mongoose.connect("mongodb+srv://<username>:<password>@cluster0.eyhty.mongodb.net/myFirstDatabase?retryWrites=true&w=majority")
You could connect to a local MongoDB instance, but for this article we are going to use a free MongoDB Atlas cluster. If you don’t already have an account, it's easy to sign up for a free MongoDB Atlas cluster here.
After creating your cluster, you should replace the connection string above with your connection string including your username and password.
The connection string that you copy from the MongoDB Atlas dashboard will reference the
myFirstDatabase
database. Change that to whatever you would like to call your database.Before we do anything with our connection, we’ll need to create a schema and model.
Ideally, you would create a schema/model file for each schema that is needed. So we’ll create a new folder/file structure:
model/Blog.js
.1 import mongoose from 'mongoose'; 2 const { Schema, model } = mongoose; 3 4 const blogSchema = new Schema({ 5 title: String, 6 slug: String, 7 published: Boolean, 8 author: String, 9 content: String, 10 tags: [String], 11 createdAt: Date, 12 updatedAt: Date, 13 comments: [{ 14 user: String, 15 content: String, 16 votes: Number 17 }] 18 }); 19 20 const Blog = model('Blog', blogSchema); 21 export default Blog;
Now that we have our first model and schema set up, we can start inserting data into our database.
Back in the
index.js
file, let’s insert a new blog article.1 import mongoose from 'mongoose'; 2 import Blog from './model/Blog'; 3 4 mongoose.connect("mongodb+srv://mongo:mongo@cluster0.eyhty.mongodb.net/myFirstDatabase?retryWrites=true&w=majority") 5 6 // Create a new blog post object 7 const article = new Blog({ 8 title: 'Awesome Post!', 9 slug: 'awesome-post', 10 published: true, 11 content: 'This is the best post ever', 12 tags: ['featured', 'announcement'], 13 }); 14 15 // Insert the article in our MongoDB database 16 await article.save();
We first need to import the
Blog
model that we created. Next, we create a new blog object and then use the save()
method to insert it into our MongoDB database.Let’s add a bit more after that to log what is currently in the database. We’ll use the
findOne()
method for this.1 // Find a single blog post 2 const firstArticle = await Blog.findOne({}); 3 console.log(firstArticle);
Let’s run the code!
1 npm run dev
You should see the document inserted logged in your terminal.
Because we are using
nodemon
in this project, every time you save a file, the code will run again. If you want to insert a bunch of articles, just keep saving. 😄In the previous example, we used the
save()
Mongoose method to insert the document into our database. This requires two actions: instantiating the object, and then saving it.Alternatively, we can do this in one action using the Mongoose
create()
method.1 // Create a new blog post and insert into database 2 const article = await Blog.create({ 3 title: 'Awesome Post!', 4 slug: 'awesome-post', 5 published: true, 6 content: 'This is the best post ever', 7 tags: ['featured', 'announcement'], 8 }); 9 10 console.log(article);
This method is much better! Not only can we insert our document, but we also get returned the document along with its
_id
when we console log it.Mongoose makes updating data very convenient too. Expanding on the previous example, let’s change the
title
of our article.1 article.title = "The Most Awesomest Post!!"; 2 await article.save(); 3 console.log(article);
We can directly edit the local object, and then use the
save()
method to write the update back to the database. I don’t think it can get much easier than that!Let’s make sure we are updating the correct document. We’ll use a special Mongoose method,
findById()
, to get our document by its ObjectId.1 const article = await Blog.findById("62472b6ce09e8b77266d6b1b").exec(); 2 console.log(article);
Notice that we use the
exec()
Mongoose function. This is technically optional and returns a promise. In my experience, it’s better to use this function since it will prevent some head-scratching issues. If you want to read more about it, check out this note in the Mongoose docs about promises.Just like with the standard MongoDB Node.js driver, we can project only the fields that we need. Let’s only get the
title
, slug
, and content
fields.1 const article = await Blog.findById("62472b6ce09e8b77266d6b1b", "title slug content").exec(); 2 console.log(article);
The second parameter can be of type
Object|String|Array<String>
to specify which fields we would like to project. In this case, we used a String
.Just like in the standard MongoDB Node.js driver, we have the
deleteOne()
and deleteMany()
methods.1 const blog = await Blog.deleteOne({ author: "Jesse Hall" }) 2 console.log(blog) 3 4 const blog = await Blog.deleteMany({ author: "Jesse Hall" }) 5 console.log(blog)
Notice that the documents we have inserted so far have not contained an
author
, dates, or comments
. So far, we have defined what the structure of our document should look like, but we have not defined which fields are actually required. At this point any field can be omitted.Let’s set some required fields in our
Blog.js
schema.1 const blogSchema = new Schema({ 2 title: { 3 type: String, 4 required: true, 5 }, 6 slug: { 7 type: String, 8 required: true, 9 lowercase: true, 10 }, 11 published: { 12 type: Boolean, 13 default: false, 14 }, 15 author: { 16 type: String, 17 required: true, 18 }, 19 content: String, 20 tags: [String], 21 createdAt: { 22 type: Date, 23 default: () => Date.now(), 24 immutable: true, 25 }, 26 updatedAt: Date, 27 comments: [{ 28 user: String, 29 content: String, 30 votes: Number 31 }] 32 });
When including validation on a field, we pass an object as its value.
value: String
is the same as value: {type: String}
.There are several validation methods that can be used.
We can set
required
to true on any fields we would like to be required.For the
slug
, we want the string to always be in lowercase. For this, we can set lowercase
to true. This will take the slug input and convert it to lowercase before saving the document to the database.For our
created
date, we can set the default buy using an arrow function. We also want this date to be impossible to change later. We can do that by setting immutable
to true.Validators only run on the create or save methods.
Mongoose uses many standard MongoDB methods plus introduces many extra helper methods that are abstracted from regular MongoDB methods. Next, we’ll go over just a few of them.
The
exists()
method returns either null
or the ObjectId of a document that matches the provided query.1 const blog = await Blog.exists({ author: "Jesse Hall" }) 2 console.log(blog)
Mongoose also has its own style of querying data. The
where()
method allows us to chain and build queries.1 // Instead of using a standard find method 2 const blogFind = await Blog.findOne({ author: "Jesse Hall" }); 3 4 // Use the equivalent where() method 5 const blogWhere = await Blog.where("author").equals("Jesse Hall"); 6 console.log(blogWhere)
Either of these methods work. Use whichever seems more natural to you.
You can also chain multiple
where()
methods to include even the most complicated query.To include projection when using the
where()
method, chain the select()
method after your query.1 const blog = await Blog.where("author").equals("Jesse Hall").select("title author") 2 console.log(blog)
It's important to understand your options when modeling data.
If you’re coming from a relational database background, you’ll be used to having separate tables for all of your related data.
Generally, in MongoDB, data that is accessed together should be stored together.
You should plan this out ahead of time if possible. Nest data within the same schema when it makes sense.
If you have the need for separate schemas, Mongoose makes it a breeze.
Let’s create another schema so that we can see how multiple schemas can be used together.
We’ll create a new file,
User.js
, in the model folder.1 import mongoose from 'mongoose'; 2 const {Schema, model} = mongoose; 3 4 const userSchema = new Schema({ 5 name: { 6 type: String, 7 required: true, 8 }, 9 email: { 10 type: String, 11 minLength: 10, 12 required: true, 13 lowercase: true 14 }, 15 }); 16 17 const User = model('User', userSchema); 18 export default User;
For the
email
, we are using a new property, minLength
, to require a minimum character length for this string.Now we’ll reference this new user model in our blog schema for the
author
and comments.user
.1 import mongoose from 'mongoose'; 2 const { Schema, SchemaTypes, model } = mongoose; 3 4 const blogSchema = new Schema({ 5 ..., 6 author: { 7 type: SchemaTypes.ObjectId, 8 ref: 'User', 9 required: true, 10 }, 11 ..., 12 comments: [{ 13 user: { 14 type: SchemaTypes.ObjectId, 15 ref: 'User', 16 required: true, 17 }, 18 content: String, 19 votes: Number 20 }]; 21 }); 22 ...
Here, we set the
author
and comments.user
to SchemaTypes.ObjectId
and added a ref
, or reference, to the user model.This will allow us to “join” our data a bit later.
And don’t forget to destructure
SchemaTypes
from mongoose
at the top of the file.Lastly, let’s update the
index.js
file. We’ll need to import our new user model, create a new user, and create a new article with the new user’s _id
.1 ... 2 import User from './model/User.js'; 3 4 ... 5 6 const user = await User.create({ 7 name: 'Jesse Hall', 8 email: 'jesse@email.com', 9 }); 10 11 const article = await Blog.create({ 12 title: 'Awesome Post!', 13 slug: 'Awesome-Post', 14 author: user._id, 15 content: 'This is the best post ever', 16 tags: ['featured', 'announcement'], 17 }); 18 19 console.log(article);
Notice now that there is a
users
collection along with the blogs
collection in the MongoDB database.You’ll now see only the user
_id
in the author field. So, how do we get all of the info for the author along with the article?We can use the
populate()
Mongoose method.1 const article = await Blog.findOne({ title: "Awesome Post!" }).populate("author"); 2 console.log(article);
Now the data for the
author
is populated, or “joined,” into the article
data. Mongoose actually uses the MongoDB $lookup
method behind the scenes.In Mongoose, middleware are functions that run before and/or during the execution of asynchronous functions at the schema level.
Here’s an example. Let’s update the
updated
date every time an article is saved or updated. We’ll add this to our Blog.js
model.1 blogSchema.pre('save', function(next) { 2 this.updated = Date.now(); // update the date every time a blog post is saved 3 next(); 4 });
Then in the
index.js
file, we’ll find an article, update the title, and then save it.1 const article = await Blog.findById("6247589060c9b6abfa1ef530").exec(); 2 article.title = "Updated Title"; 3 await article.save(); 4 console.log(article);
Notice that we now have an
updated
date!Besides
pre()
, there is also a post()
mongoose middleware function.I think our example here could use another schema for the
comments
. Try creating that schema and testing it by adding a few users and comments.There are many other great Mongoose helper methods that are not covered here. Be sure to check out the official documentation for references and more examples.
I think it’s great that developers have many options for connecting and manipulating data in MongoDB. Whether you prefer Mongoose or the standard MongoDB drivers, in the end, it’s all about the data and what’s best for your application and use case.
I can see why Mongoose appeals to many developers and I think I’ll use it more in the future.