CRUD Operations with MongoDB and Mongoose
How to perform Create, Read, Update, and Delete operations in MongoDB using Mongoose ODM with Node.js and Express
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
CRUD Operations with MongoDB and Mongoose
Mongoose provides a schema-based solution to model application data for MongoDB. It handles type casting, validation, query building, and business logic hooks, making it the most popular ODM in the Node.js ecosystem.
When to Use This
- You need a structured way to interact with MongoDB from Node.js
- You want automatic validation and middleware hooks
- You are building an API that requires relational-like patterns in a document database
Prerequisites
- MongoDB installed locally or a MongoDB Atlas cluster
- Node.js 18+
Solution: Express + Mongoose
1. Install Dependencies
npm install express mongoose dotenv
2. Define the Schema
// models/User.js
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true,
},
name: {
type: String,
required: true,
minlength: 2,
maxlength: 100,
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user',
},
isActive: {
type: Boolean,
default: true,
},
}, {
timestamps: true,
});
// Index for common queries
userSchema.index({ email: 1 });
userSchema.index({ role: 1, isActive: 1 });
export default mongoose.model('User', userSchema);
3. Connect and Perform CRUD
// app.js
import express from 'express';
import mongoose from 'mongoose';
import User from './models/User.js';
const app = express();
app.use(express.json());
await mongoose.connect(process.env.MONGODB_URI);
// CREATE
app.post('/users', async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// READ (with pagination)
app.get('/users', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const [users, total] = await Promise.all([
User.find({ isActive: true })
.select('-__v')
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.lean(),
User.countDocuments({ isActive: true }),
]);
res.json({
data: users,
pagination: { page, limit, total, pages: Math.ceil(total / limit) },
});
});
// READ ONE
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id).select('-__v');
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
// UPDATE
app.patch('/users/:id', async (req, res) => {
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);
});
// DELETE (soft delete)
app.delete('/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
{ isActive: false },
{ new: true }
);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json({ message: 'User deactivated' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
4. Transactions
// Atomic operations across collections
app.post('/orders', async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
const order = await Order.create([{ ...req.body, status: 'pending' }], { session });
await Product.updateOne(
{ _id: req.body.productId },
{ $inc: { stock: -req.body.quantity } },
{ session }
);
await session.commitTransaction();
res.status(201).json(order[0]);
} catch (err) {
await session.abortTransaction();
res.status(400).json({ error: err.message });
} finally {
session.endSession();
}
});
How It Works
- Schema Definition enforces structure while preserving MongoDB’s flexibility
- Middleware Hooks run validation and transformation before/after operations
- Query Building provides a chainable API for complex queries
- Transactions ensure ACID compliance across multiple documents
Production Considerations
- Enable read preference
secondaryfor read-heavy workloads in replica sets - Use compound indexes for frequently combined query fields
- Implement cursor-based pagination for large datasets instead of skip/limit
- Add Mongoose plugins for common patterns (soft delete, auditing)
FAQ
Q: Should I use Mongoose or the native driver? A: Use Mongoose for application data with validation needs. Use the native driver for analytics, aggregation pipelines, or maximum performance.
Q: How do I handle schema migrations?
A: Use migrate-mongo or write idempotent migration scripts that run on deployment.
Q: When should I use references vs embedded documents? A: Embed when data is read together and unbounded growth is not expected. Reference when data is updated independently or grows without limit.
Related Resources
Database Design Guide
A practical guide to designing relational databases with normalization, indexing, and relationship modeling.
RecipeOptimize Queries with Database Indexing
How to create, analyze, and maintain indexes to speed up database queries and avoid common indexing mistakes.
PatternRepository Pattern
Abstract data access logic behind a clean interface. An architectural design pattern for testable, maintainable data layers.