본문으로 건너뛰기 Mongoose Complete Guide | MongoDB ODM for Node.js

Mongoose Complete Guide | MongoDB ODM for Node.js

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:

  1. Define schemas for validation
  2. Use middleware for hooks
  3. Populate for relationships
  4. Lean queries for performance
  5. Index frequently queried fields

Next Steps:

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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Mongoose, MongoDB, Node.js, Database, ODM, TypeScript.