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: