What is Mongoose?
Mongoose is an ODM (Object Document Mapping) library for MongoDB and Node.js. It provides a schema-based solution to model your data with built-in validation, query building, and middleware (hooks).
Express App Mongoose MongoDB
│ │ │
│ User.find({}) │ │
│──────────────────────→│ │
│ │ find() │
│ │──────────────────────→│
│ │ │
│ │←────── documents ─────│
│←─── Model instances ──│ │
│ │ │
Installation
npm install mongoose
Connecting to MongoDB
const mongoose = require('mongoose');
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/myapp';
mongoose.connect(MONGO_URI)
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
Connection Options
mongoose.connect(MONGO_URI, {
serverSelectionTimeoutMS: 5000, // Timeout after 5s
socketTimeoutMS: 45000, // Close sockets after 45s of inactivity
family: 4 // Use IPv4, skip IPv6
});
Best Practice: Reusable Connection Module
config/database.js:
const mongoose = require('mongoose');
async function connectDB() {
const uri = process.env.MONGO_URI || 'mongodb://localhost:27017/myapp';
try {
await mongoose.connect(uri);
console.log(`MongoDB connected: ${mongoose.connection.host}`);
} catch (err) {
console.error('MongoDB connection failed:', err.message);
process.exit(1);
}
}
module.exports = connectDB;
index.js:
const express = require('express');
const connectDB = require('./config/database');
const app = express();
// Connect to database before starting server
connectDB();
app.listen(3000);
Connection Events
mongoose.connection.on('connected', () => {
console.log('Mongoose connected to DB');
});
mongoose.connection.on('error', (err) => {
console.error('Mongoose connection error:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('Mongoose disconnected');
});
// Graceful shutdown
process.on('SIGINT', async () => {
await mongoose.connection.close();
process.exit(0);
});
Defining a Schema
A schema defines the structure of documents in a collection.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
minlength: [2, 'Name must be at least 2 characters'],
maxlength: [50, 'Name cannot exceed 50 characters']
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, 'Please provide a valid email']
},
age: {
type: Number,
min: [13, 'Must be at least 13 years old'],
max: [120, 'Age cannot exceed 120']
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', userSchema);
Schema Types
| Type | Description |
|---|---|
String | UTF-8 string |
Number | 64-bit float |
Boolean | true/false |
Date | Date object |
Buffer | Binary data |
ObjectId | Unique MongoDB identifier |
Array | Nested array |
Mixed | Any type (no validation) |
Map | Key-value pairs |
Decimal128 | High-precision decimal |
Schema.Types.ObjectId | Reference to another document |
Creating a Model
A model is a compiled version of the schema that provides an interface to the database.
const User = mongoose.model('User', userSchema);
// Now you can do: User.find(), User.create(), etc.
CRUD Operations with Mongoose
Create
// Method 1: Create and save
const user = new User({
name: 'Alice',
email: 'alice@example.com',
age: 30
});
await user.save();
// Method 2: Create directly
const user = await User.create({
name: 'Bob',
email: 'bob@example.com',
age: 25
});
Read
// Find all
const users = await User.find();
// Find with filters
const adults = await User.find({ age: { $gte: 18 } });
// Find one
const user = await User.findOne({ email: 'alice@example.com' });
// Find by ID
const user = await User.findById('507f1f77bcf86cd799439011');
// Select specific fields
const users = await User.find().select('name email');
// Sort, limit, skip (pagination)
const users = await User.find()
.sort({ createdAt: -1 })
.limit(10)
.skip(20);
Update
// Find and update (returns old document by default)
const user = await User.findByIdAndUpdate(
id,
{ name: 'Updated Name' },
{ new: true, runValidators: true }
);
// Update one (returns update result, not the document)
const result = await User.updateOne(
{ email: 'alice@example.com' },
{ $set: { age: 31 } }
);
// Update many
await User.updateMany(
{ role: 'user' },
{ $set: { isActive: true } }
);
Delete
// Find and delete
const user = await User.findByIdAndDelete(id);
// Delete one
await User.deleteOne({ email: 'old@example.com' });
// Delete many
await User.deleteMany({ isActive: false });
Schema with References (Relationships)
// models/post.js
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
body: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // Reference to User model
required: true
},
tags: [String],
likes: {
type: Number,
default: 0
},
comments: [{
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
text: String,
createdAt: { type: Date, default: Date.now }
}]
}, {
timestamps: true // Adds createdAt and updatedAt automatically
});
module.exports = mongoose.model('Post', postSchema);
Populating References (Joins)
// Get posts with author data populated
const posts = await Post.find()
.populate('author', 'name email')
.populate('comments.user', 'name');
// Populate nested
const posts = await Post.find().populate({
path: 'author',
populate: { path: 'profile', select: 'avatar bio' }
});
Schema Middleware (Hooks)
const bcrypt = require('bcrypt');
userSchema.pre('save', async function(next) {
// Only hash if password is modified
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
userSchema.post('save', function(doc) {
console.log(`User ${doc.email} was saved`);
});
userSchema.pre('find', function() {
// `this` is the query - filter out inactive by default
this.where({ isActive: true });
});
Virtual Properties
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
userSchema.virtual('isAdult').get(function() {
return this.age >= 18;
});
// Virtuals are not included in JSON by default
// To include them:
userSchema.set('toJSON', { virtuals: true });
userSchema.set('toObject', { virtuals: true });
Full Integration Example
const express = require('express');
const mongoose = require('mongoose');
require('./models/User');
require('./models/Post');
const app = express();
app.use(express.json());
// Connect
mongoose.connect(process.env.MONGO_URI);
// Routes
const User = mongoose.model('User');
app.post('/api/users', async (req, res, next) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (err) {
next(err);
}
});
app.get('/api/users', async (req, res, next) => {
try {
const users = await User.find().select('-__v');
res.json(users);
} catch (err) {
next(err);
}
});
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
} catch (err) {
next(err);
}
});
app.put('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
} catch (err) {
next(err);
}
});
app.delete('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.status(204).send();
} catch (err) {
next(err);
}
});
app.listen(3000);
Key Takeaways
- Mongoose provides schema-based modeling with built-in validation
- Connect with
mongoose.connect(URI)in a reusable module - Schemas define structure; Models provide the query interface
- Use
populate()for referencing documents in other collections - Middleware hooks (
pre/post) run logic before/after operations - Virtuals compute derived properties without storing them
- Always use
try/catchor middleware for async error handling - Store connection string in an environment variable (
MONGO_URI)