While the MongoDB driver is great, it's also very low-level. For most applications, you'll want a higher-level abstraction to work with your data. Enter Mongoose, the most popular Object Data Modeling (ODM) library for MongoDB and Node.js.
Mongoose helps you by providing:
- A schema-based structure for your documents.
- Built-in type casting and validation.
- Middleware hooks to add custom logic.
- A rich API for querying the database.
Schemas and Models: The Blueprint and the Factory
This is the core concept of Mongoose.
- Schema: The blueprint. It's a JavaScript object that defines the structure of your documents—what fields they have, what data types those fields should be, default values, and validation rules.
- Model: The factory. A model is a constructor that is compiled from a Schema definition. You use the model to create, find, update, and delete documents of that type.
Code Snippet: Defining a User
JavaScript
import mongoose from 'mongoose';
// 1. Define the Schema (the blueprint)
const userSchema = new mongoose.Schema({
firstName: { type: String, required: true },
lastName: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: { type: Number, min: 18 },
createdAt: { type: Date, default: () => Date.now() }
});
// 2. Compile the Model from the Schema (the factory)
const User = mongoose.model('User', userSchema);
// Now you can use the 'User' model to interact with the 'users' collection
// For example: const newUser = new User({ ... });
Validation: Enforcing Data Integrity
Mongoose's schemas allow you to define powerful validation rules right where you define your data structure. Mongoose will automatically check these rules before saving a document.
Code Snippet: Advanced Validation
Let's enhance our userSchema with more validation.
JavaScript
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, "Email is required."], // Custom error message
unique: true,
lowercase: true, // Mongoose modifier
match: [/\S+@\S+\.\S+/, 'is invalid'] // Regex validation
},
role: {
type: String,
enum: ['user', 'admin'], // Must be one of these values
default: 'user'
},
age: {
type: Number,
min: [13, "Must be at least 13 years old."],
max: 120
}
});
If you try to save a user with an invalid email or an age of 10, Mongoose will throw a validation error, preventing bad data from entering your database.
Middleware (Hooks): Adding Custom Logic
Middleware (also called "pre" and "post" hooks) are functions that are executed at specific points in a document's lifecycle. This is incredibly powerful for tasks like data transformation, logging, or complex validation.
The most common hook is pre('save'), which runs just before a document is saved to the database.
Code Snippet: Hashing a Password Before Saving
This is the classic example of middleware. We never want to store plain-text passwords. A pre-save hook is the perfect place to hash the password.
JavaScript
import bcrypt from 'bcrypt';
const userSchema = new mongoose.Schema({
// ... other fields
password: { type: String, required: true, minlength: 8 }
});
// Define a pre-save middleware hook
userSchema.pre('save', async function(next) {
// 'this' refers to the document being saved
const user = this;
// Only hash the password if it has been modified (or is new)
if (!user.isModified('password')) return next();
try {
// Generate a salt and hash the password
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(user.password, salt);
// Replace the plain text password with the hash
user.password = hash;
next(); // Continue with the save operation
} catch (error) {
next(error);
}
});
const User = mongoose.model('User', userSchema);
Now, whenever you call newUser.save(), this function will automatically run, ensuring your passwords are always securely stored.