In the previous module, we used the official MongoDB Native Driver to connect our Node.js app to the database. While the Native Driver is extremely fast, it has one major flaw: it lets you do whatever you want.
Because MongoDB is schema-less, the Native Driver will happily let you insert a user with a username, and then insert another user with a firstName and lastName, and then insert a third user with a boolean instead of a string. This lack of structure can quickly lead to chaotic, unmaintainable code in a large application.
To solve this, the Node.js community built Mongoose.
What is Mongoose?
Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js.
It sits on top of the Native Driver and forces structure onto your schema-less database. It provides rigorous validation, relationship modeling, and powerful features that make writing business logic significantly easier.
If you are building a Node.js application today, there is a 95% chance you will be using Mongoose.
To get started, install it via NPM:
npm install mongoose
Connecting with Mongoose is even simpler than the Native Driver:
import mongoose from 'mongoose';
mongoose.connect('mongodb://localhost:27017/library')
.then(() => console.log("Connected to MongoDB via Mongoose!"))
.catch(err => console.error("Connection failed", err));
Schemas and Models
Everything in Mongoose starts with a Schema. A Schema defines the exact shape of your documents, including what fields are allowed and what data types they must be.
Once you have a Schema, you compile it into a Model. A Model is a special JavaScript class that represents your collection and allows you to query the database.
Let’s create a User model:
import mongoose from 'mongoose';
// 1. Define the Schema
const userSchema = new mongoose.Schema({
username: String,
email: String,
age: Number,
createdAt: { type: Date, default: Date.now } // Automatically sets the date!
});
// 2. Compile the Model (Creates a 'users' collection automatically)
const User = mongoose.model('User', userSchema);
// 3. Use the Model to interact with the database
async function createUser() {
// Create a new instance of the User class
const newUser = new User({
username: "coder123",
email: "coder@example.com",
age: 25
});
// Save it to the database
await newUser.save();
console.log("User saved successfully!");
}
Validation
The greatest superpower of Mongoose is data validation. If a user tries to submit an invalid form on your website, Mongoose will catch it and throw a detailed error before the bad data ever touches the database.
You can configure strict rules directly in your Schema:
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, 'Username is required!'], // Cannot be null
minlength: [3, 'Username must be at least 3 characters'],
maxlength: 20,
trim: true // Automatically removes whitespace from the edges
},
email: {
type: String,
required: true,
unique: true, // Automatically creates a Unique Index in MongoDB!
lowercase: true // Converts "User@Example.com" to "user@example.com"
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'], // Only these 3 strings are allowed
default: 'user'
}
});
If you try to save a user with role: "superhacker", Mongoose will immediately throw a ValidationError and block the save operation.
Middleware (Hooks)
Middleware (also known as Hooks) are functions that automatically run at specific times during the lifecycle of a document.
The two most common hooks are pre (runs before an event) and post (runs after an event).
Real-world Example: Hashing a Password
You should never save a plain-text password to the database. We can use a Mongoose pre('save') hook to automatically hash the user’s password right before it gets saved.
import bcrypt from 'bcrypt';
// This function runs automatically BEFORE every .save()
userSchema.pre('save', async function(next) {
// 'this' refers to the document being saved
// If the password hasn't been modified, skip this hook
if (!this.isModified('password')) return next();
// Hash the password with bcrypt
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next(); // Tell Mongoose to proceed with saving to the database
});
Now, you never have to remember to hash the password in your application code. Mongoose handles it invisibly!
Virtuals
A Virtual is a property that exists on your Mongoose Model, but does not actually get saved to the MongoDB database.
Why is this useful? Imagine you have a firstName and lastName field. Every time you show the user’s name on the website, you have to concatenate them together (user.firstName + " " + user.lastName).
Instead, you can create a fullName Virtual:
const userSchema = new mongoose.Schema({
firstName: String,
lastName: String
});
// Create a Virtual property
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
const User = mongoose.model('User', userSchema);
// In your application code:
const user = await User.findOne();
console.log(user.fullName); // Prints "John Doe"
The fullName property acts exactly like a normal field in your JavaScript code, but it doesn’t waste any storage space in your database!
Summary
Mongoose turns MongoDB from a wild, schema-less data store into a highly disciplined, easy-to-use engine for enterprise Node.js applications.
- Schemas and Models define the shape of your data.
- Validation ensures junk data never reaches your database.
- Middleware (Hooks) automates tasks like password hashing.
- Virtuals compute properties on the fly without wasting database storage.
In Module 22, we will explore another powerful ecosystem tool. We will learn how to use MongoDB Prisma and GraphQL!
Discussion
Loading comments...