Mongoose Complete Guide | MongoDB ODM for Node.js
이 글의 핵심
Mongoose is a MongoDB object modeling tool for Node.js. It provides schema-based validation, middleware, queries, and a elegant API for working with MongoDB.
Introduction
Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It provides a schema-based solution to model your data with built-in validation, queries, and business logic.
Why Mongoose?
Native MongoDB driver:
const db = client.db('myapp');
const users = db.collection('users');
// No schema, no validation
await users.insertOne({ name: 'Alice', age: 'invalid' }); // Inserts anything!
With Mongoose:
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
age: { type: Number, min: 0 },
});
const User = mongoose.model('User', userSchema);
await User.create({ name: 'Alice', age: 'invalid' }); // Validation error!
1. Installation
npm install mongoose
2. Connection
const mongoose = require('mongoose');
// Connect to MongoDB
await mongoose.connect('mongodb://localhost:27017/myapp');
// Or with options
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Handle connection events
mongoose.connection.on('connected', () => {
console.log('MongoDB connected');
});
mongoose.connection.on('error', (err) => {
console.error('MongoDB error:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB disconnected');
});
3. Schema Definition
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
// String
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
minlength: 2,
maxlength: 50,
},
// Email with validation
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email format'],
},
// Number
age: {
type: Number,
min: [0, 'Age must be positive'],
max: 120,
},
// Enum
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user',
},
// Boolean
isActive: {
type: Boolean,
default: true,
},
// Date
createdAt: {
type: Date,
default: Date.now,
},
// Array
tags: [String],
// Nested object
address: {
street: String,
city: String,
zipCode: String,
},
// Reference to another model
posts: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Post',
}],
});
const User = mongoose.model('User', userSchema);
4. CRUD Operations
Create
// Method 1: create()
const user = await User.create({
name: 'Alice',
email: 'alice@example.com',
age: 30,
});
// Method 2: new + save()
const user = new User({
name: 'Bob',
email: 'bob@example.com',
});
await user.save();
// Create multiple
await User.insertMany([
{ name: 'Charlie', email: 'charlie@example.com' },
{ name: 'David', email: 'david@example.com' },
]);
Read
// Find all
const users = await User.find();
// Find with filter
const activeUsers = await User.find({ isActive: true });
// Find one
const user = await User.findOne({ email: 'alice@example.com' });
// Find by ID
const user = await User.findById('507f1f77bcf86cd799439011');
// With projection (select fields)
const users = await User.find().select('name email');
// With sorting
const users = await User.find().sort({ createdAt: -1 });
// With limit and skip (pagination)
const users = await User.find()
.limit(10)
.skip(20)
.sort({ createdAt: -1 });
// Count
const count = await User.countDocuments({ isActive: true });
Update
// Update one
await User.updateOne(
{ email: 'alice@example.com' },
{ $set: { age: 31 } }
);
// Update many
await User.updateMany(
{ isActive: false },
{ $set: { isActive: true } }
);
// Find and update (returns updated document)
const user = await User.findOneAndUpdate(
{ email: 'alice@example.com' },
{ $set: { age: 31 } },
{ new: true } // Return updated document
);
// Update by ID
const user = await User.findByIdAndUpdate(
'507f1f77bcf86cd799439011',
{ $set: { age: 31 } },
{ new: true }
);
Delete
// Delete one
await User.deleteOne({ email: 'alice@example.com' });
// Delete many
await User.deleteMany({ isActive: false });
// Find and delete (returns deleted document)
const user = await User.findOneAndDelete({ email: 'alice@example.com' });
// Delete by ID
await User.findByIdAndDelete('507f1f77bcf86cd799439011');
5. Query Operators
// Comparison
User.find({ age: { $gt: 18 } }); // Greater than
User.find({ age: { $gte: 18 } }); // Greater than or equal
User.find({ age: { $lt: 65 } }); // Less than
User.find({ age: { $lte: 65 } }); // Less than or equal
User.find({ age: { $ne: 30 } }); // Not equal
// Logical
User.find({
$and: [
{ age: { $gte: 18 } },
{ age: { $lte: 65 } }
]
});
User.find({
$or: [
{ role: 'admin' },
{ role: 'moderator' }
]
});
// In/Not in
User.find({ role: { $in: ['admin', 'moderator'] } });
User.find({ role: { $nin: ['banned', 'suspended'] } });
// Exists
User.find({ email: { $exists: true } });
// Regex
User.find({ name: { $regex: /^A/i } }); // Names starting with A
6. Middleware (Hooks)
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
email: String,
password: String,
});
// Pre-save hook
userSchema.pre('save', async function(next) {
// Only hash if password is modified
if (!this.isModified('password')) return next();
// Hash password
this.password = await bcrypt.hash(this.password, 10);
next();
});
// Post-save hook
userSchema.post('save', function(doc) {
console.log('User saved:', doc._id);
});
// Pre-remove hook
userSchema.pre('remove', async function(next) {
// Delete user's posts
await Post.deleteMany({ author: this._id });
next();
});
const User = mongoose.model('User', userSchema);
// Usage
const user = new User({ email: 'user@example.com', password: 'password123' });
await user.save(); // Password automatically hashed
7. Instance Methods
const userSchema = new mongoose.Schema({
email: String,
password: String,
});
// Add instance method
userSchema.methods.verifyPassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
userSchema.methods.toPublicJSON = function() {
return {
id: this._id,
email: this.email,
// Don't include password
};
};
const User = mongoose.model('User', userSchema);
// Usage
const user = await User.findOne({ email: 'alice@example.com' });
const isValid = await user.verifyPassword('password123');
const publicData = user.toPublicJSON();
8. Static Methods
// Add static method
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
userSchema.statics.findActiveUsers = function() {
return this.find({ isActive: true });
};
const User = mongoose.model('User', userSchema);
// Usage
const user = await User.findByEmail('Alice@Example.com');
const activeUsers = await User.findActiveUsers();
9. Virtual Fields
const userSchema = new mongoose.Schema({
firstName: String,
lastName: String,
});
// Virtual property (not stored in DB)
userSchema.virtual('fullName')
.get(function() {
return `${this.firstName} ${this.lastName}`;
})
.set(function(name) {
const parts = name.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
});
// Enable virtuals in JSON
userSchema.set('toJSON', { virtuals: true });
const User = mongoose.model('User', userSchema);
// Usage
const user = new User({ firstName: 'Alice', lastName: 'Smith' });
console.log(user.fullName); // "Alice Smith"
user.fullName = 'Bob Johnson';
console.log(user.firstName); // "Bob"
console.log(user.lastName); // "Johnson"
10. Population (Relations)
const postSchema = new mongoose.Schema({
title: String,
content: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
});
const Post = mongoose.model('Post', postSchema);
// Create post
const post = await Post.create({
title: 'My First Post',
content: 'Hello world',
author: user._id, // User's ObjectId
});
// Populate author
const postWithAuthor = await Post.findById(post._id).populate('author');
console.log(postWithAuthor.author.name); // "Alice"
// Populate specific fields
const post = await Post.findById(postId)
.populate('author', 'name email');
// Nested populate
const post = await Post.findById(postId)
.populate({
path: 'author',
populate: { path: 'company' }
});
11. Validation
const productSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Product name is required'],
minlength: [3, 'Name must be at least 3 characters'],
},
price: {
type: Number,
required: true,
min: [0, 'Price must be positive'],
validate: {
validator: function(v) {
return v > 0;
},
message: 'Price must be greater than 0',
},
},
category: {
type: String,
enum: {
values: ['electronics', 'clothing', 'food'],
message: '{VALUE} is not a valid category',
},
},
email: {
type: String,
validate: {
validator: function(v) {
return /^\S+@\S+\.\S+$/.test(v);
},
message: props => `${props.value} is not a valid email`,
},
},
});
const Product = mongoose.model('Product', productSchema);
// Validation happens on save
try {
await Product.create({ name: 'AB', price: -10 });
} catch (error) {
console.error(error.errors);
// ValidationError: name: Name must be at least 3 characters
// ValidationError: price: Price must be positive
}
12. TypeScript Integration
import mongoose, { Document, Schema } from 'mongoose';
interface IUser extends Document {
name: string;
email: string;
age: number;
createdAt: Date;
verifyPassword(password: string): Promise<boolean>;
}
const userSchema = new Schema<IUser>({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: { type: Number, min: 0 },
createdAt: { type: Date, default: Date.now },
});
userSchema.methods.verifyPassword = async function(password: string) {
return await bcrypt.compare(password, this.password);
};
const User = mongoose.model<IUser>('User', userSchema);
// Usage with full type safety
const user: IUser = await User.create({
name: 'Alice',
email: 'alice@example.com',
age: 30,
});
console.log(user.name); // TypeScript knows it's a string
const isValid = await user.verifyPassword('password123'); // TypeScript knows the method
13. Real-World Example: Blog API
const mongoose = require('mongoose');
// User schema
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
createdAt: { type: Date, default: Date.now },
});
userSchema.methods.verifyPassword = async function(password) {
return await bcrypt.compare(password, this.password);
};
const User = mongoose.model('User', userSchema);
// Post schema
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
slug: { type: String, required: true, unique: true },
content: { type: String, required: true },
excerpt: { type: String, maxlength: 300 },
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
tags: [String],
published: { type: Boolean, default: false },
views: { type: Number, default: 0 },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
});
// Auto-update updatedAt
postSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
// Generate slug from title
postSchema.pre('save', function(next) {
if (this.isModified('title')) {
this.slug = this.title.toLowerCase().replace(/\s+/g, '-');
}
next();
});
const Post = mongoose.model('Post', postSchema);
// Comment schema
const commentSchema = new mongoose.Schema({
post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
content: { type: String, required: true, maxlength: 1000 },
createdAt: { type: Date, default: Date.now },
});
const Comment = mongoose.model('Comment', commentSchema);
14. Express API with Mongoose
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.json());
// Connect to MongoDB
await mongoose.connect(process.env.MONGODB_URI);
// Get all posts
app.get('/api/posts', async (req, res) => {
try {
const { page = 1, limit = 10 } = req.query;
const posts = await Post.find({ published: true })
.populate('author', 'username')
.sort({ createdAt: -1 })
.limit(limit)
.skip((page - 1) * limit);
const total = await Post.countDocuments({ published: true });
res.json({
posts,
page: parseInt(page),
totalPages: Math.ceil(total / limit),
total,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get single post
app.get('/api/posts/:slug', async (req, res) => {
try {
const post = await Post.findOne({ slug: req.params.slug })
.populate('author', 'username email');
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
// Increment views
post.views += 1;
await post.save();
res.json({ post });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Create post
app.post('/api/posts', authenticate, async (req, res) => {
try {
const post = await Post.create({
...req.body,
author: req.user.id,
});
res.status(201).json({ post });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Update post
app.put('/api/posts/:id', authenticate, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
// Check ownership
if (post.author.toString() !== req.user.id) {
return res.status(403).json({ error: 'Unauthorized' });
}
Object.assign(post, req.body);
await post.save();
res.json({ post });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Delete post
app.delete('/api/posts/:id', authenticate, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
if (post.author.toString() !== req.user.id) {
return res.status(403).json({ error: 'Unauthorized' });
}
await post.remove();
res.json({ message: 'Post deleted' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000);
15. Indexes
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
username: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
});
// Single field index
userSchema.index({ email: 1 });
// Compound index
userSchema.index({ username: 1, createdAt: -1 });
// Text index for search
postSchema.index({ title: 'text', content: 'text' });
// Usage
const posts = await Post.find({ $text: { $search: 'mongodb tutorial' } });
16. Best Practices
1. Use Lean for Read-Only Queries
// Regular query (returns Mongoose document)
const user = await User.findById(id);
user.save(); // Has methods
// Lean query (returns plain JS object - faster)
const user = await User.findById(id).lean();
// user.save() is undefined - no methods
2. Select Only Needed Fields
// Bad: fetch everything
const users = await User.find();
// Good: select specific fields
const users = await User.find().select('name email');
3. Use Pagination
async function getUsers(page = 1, limit = 10) {
const users = await User.find()
.limit(limit)
.skip((page - 1) * limit)
.sort({ createdAt: -1 });
const total = await User.countDocuments();
return {
users,
page,
totalPages: Math.ceil(total / limit),
};
}
Summary
Mongoose provides powerful MongoDB abstraction:
- Schema validation for data integrity
- Middleware for business logic
- Relationships via population
- TypeScript support
- Production-ready and battle-tested
Key Takeaways:
- Define schemas for validation
- Use middleware for hooks
- Populate for relationships
- Lean queries for performance
- Index frequently queried fields
Next Steps:
- Learn MongoDB
- Compare Prisma
- Build Express API
Resources:
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Mongoose is a MongoDB object modeling tool for Node.js. It provides schema-based validation, middleware, queries, and a elegant API for working with MongoDB.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Joi Complete Guide
- GraphQL Complete Guide | Schema· Resolver
- Yup Complete Guide | Schema Validation for JavaScript
Keywords Covered in This Article (Related Search Terms)
This article covers Mongoose, MongoDB, Node.js, Database, ODM, TypeScript.