Express Mongoose ODM
Introduction
In web development, efficiently storing and retrieving data is crucial. While Express.js provides a robust framework for building web applications, it doesn't come with built-in database functionality. This is where Mongoose comes in.
Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js that provides a straightforward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box.
In this tutorial, you'll learn:
- What Mongoose is and why it's useful
- Setting up Mongoose with Express
- Creating schemas and models
- Performing CRUD operations
- Best practices and common patterns
What is Mongoose ODM?
Mongoose is an elegant MongoDB object modeling tool designed to work in an asynchronous environment. It provides a straight-forward, schema-based solution to model your application data and includes built-in type casting, validation, query building, business logic hooks and more.
Key Benefits of Mongoose
- Schema Validation: Define the shape of your documents with strong typing
- Middleware: Execute code before or after certain operations
- Query Building: Powerful, chainable query API
- Virtual Properties: Define properties that aren't stored in MongoDB
- Data Population: Reference documents in other collections
Setting Up Mongoose with Express
Let's start by setting up a basic Express application with Mongoose integration.
Installation
First, install the required packages:
npm install express mongoose
Connecting to MongoDB
Create your Express application and establish a MongoDB connection:
const express = require('express');
const mongoose = require('mongoose');
const app = express();
// Middleware for parsing JSON
app.use(express.json());
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB connection successful'))
.catch(err => console.error('MongoDB connection error:', err));
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Connection Options
The connection options in Mongoose are important for properly configuring your MongoDB connection:
useNewUrlParser
: Use the new URL parser instead of the deprecated oneuseUnifiedTopology
: Use the new Server Discovery and Monitoring engineuseCreateIndex
: Use createIndex() instead of ensureIndex() (deprecated)useFindAndModify
: Set to false to make findOneAndUpdate() and findOneAndRemove() use native findOneAndUpdate() rather than findAndModify()
Creating Schemas and Models
Defining a Schema
Schemas are the blueprint of your documents. They define the structure, default values, validators, etc.
const mongoose = require('mongoose');
const { Schema } = mongoose;
const userSchema = new Schema({
firstName: {
type: String,
required: true,
trim: true
},
lastName: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
age: {
type: Number,
min: 18,
max: 100
},
createdAt: {
type: Date,
default: Date.now
},
isActive: {
type: Boolean,
default: true
}
});
Converting Schema to Model
Now let's convert our schema into a model:
// Create the model from the schema
const User = mongoose.model('User', userSchema);
// Export the model
module.exports = User;
Schema Types
Mongoose supports various data types:
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- ObjectId
- Array
- Decimal128
- Map
CRUD Operations with Mongoose
Let's implement basic CRUD (Create, Read, Update, Delete) operations using our User model.
Create (POST)
// In your routes file
const express = require('express');
const router = express.Router();
const User = require('../models/User');
// Create a new user
router.post('/users', async (req, res) => {
try {
const newUser = new User(req.body);
const savedUser = await newUser.save();
res.status(201).json(savedUser);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
module.exports = router;
Example Request:
POST /users
Content-Type: application/json
{
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"age": 30
}
Example Response:
{
"_id": "60a1c2b9b5f7f83e8c9d1234",
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"age": 30,
"isActive": true,
"createdAt": "2023-07-15T12:34:56.789Z",
"__v": 0
}
Read (GET)
// Get all users
router.get('/users', async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get a single user by ID
router.get('/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ message: 'User not found' });
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Update (PUT/PATCH)
// Update a user
router.put('/users/:id', async (req, res) => {
try {
const updatedUser = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!updatedUser) return res.status(404).json({ message: 'User not found' });
res.json(updatedUser);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Example Request:
PUT /users/60a1c2b9b5f7f83e8c9d1234
Content-Type: application/json
{
"firstName": "Jonathan",
"age": 31
}
Example Response:
{
"_id": "60a1c2b9b5f7f83e8c9d1234",
"firstName": "Jonathan",
"lastName": "Doe",
"email": "[email protected]",
"age": 31,
"isActive": true,
"createdAt": "2023-07-15T12:34:56.789Z",
"__v": 0
}
Delete (DELETE)
// Delete a user
router.delete('/users/:id', async (req, res) => {
try {
const deletedUser = await User.findByIdAndDelete(req.params.id);
if (!deletedUser) return res.status(404).json({ message: 'User not found' });
res.json({ message: 'User deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Advanced Mongoose Features
Validation
Mongoose has built-in validation features:
const productSchema = new 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 cannot be negative']
},
category: {
type: String,
enum: ['Electronics', 'Clothing', 'Food', 'Books'],
required: true
},
inStock: {
type: Boolean,
default: true
}
});
Custom Validation
You can also create custom validators:
const userSchema = new Schema({
email: {
type: String,
required: true,
validate: {
validator: function(v) {
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(v);
},
message: props => `${props.value} is not a valid email address!`
}
}
});
Middleware (Hooks)
Mongoose provides pre and post hooks for various operations:
userSchema.pre('save', async function(next) {
// Only hash the password if it has been modified (or is new)
if (!this.isModified('password')) return next();
try {
// Hash password with bcrypt
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
Query Building
Mongoose offers a powerful query API:
// Find active users who are 18 years or older, sort by name
const activeAdults = await User.find({
isActive: true,
age: { $gte: 18 }
})
.sort({ firstName: 1, lastName: 1 })
.select('firstName lastName email')
.limit(10)
.skip(20);
Populating References
Mongoose allows you to reference documents in other collections:
const postSchema = new Schema({
title: String,
content: String,
author: {
type: Schema.Types.ObjectId,
ref: 'User'
}
});
const Post = mongoose.model('Post', postSchema);
// Later, when querying:
const posts = await Post.find()
.populate('author', 'firstName lastName')
.exec();
Real-world Example: Blog API
Let's create a simple blog API with Mongoose and Express:
Step 1: Define Models
// models/User.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const userSchema = new Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
bio: String,
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('User', userSchema);
// models/Post.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const postSchema = new Schema({
title: { type: String, required: true },
content: { type: String, required: true },
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
tags: [String],
comments: [{
body: String,
author: {
type: Schema.Types.ObjectId,
ref: 'User'
},
createdAt: { type: Date, default: Date.now }
}],
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Post', postSchema);
Step 2: Create Routes
// routes/posts.js
const express = require('express');
const router = express.Router();
const Post = require('../models/Post');
// Get all posts
router.get('/', async (req, res) => {
try {
const posts = await Post.find()
.populate('author', 'username email')
.populate('comments.author', 'username')
.sort({ createdAt: -1 });
res.json(posts);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Create a new post
router.post('/', async (req, res) => {
const post = new Post({
title: req.body.title,
content: req.body.content,
author: req.body.author,
tags: req.body.tags
});
try {
const newPost = await post.save();
res.status(201).json(newPost);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
// Get a specific post
router.get('/:id', getPost, (req, res) => {
res.json(res.post);
});
// Update a post
router.patch('/:id', getPost, async (req, res) => {
if (req.body.title != null) {
res.post.title = req.body.title;
}
if (req.body.content != null) {
res.post.content = req.body.content;
}
if (req.body.tags != null) {
res.post.tags = req.body.tags;
}
try {
const updatedPost = await res.post.save();
res.json(updatedPost);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
// Delete a post
router.delete('/:id', getPost, async (req, res) => {
try {
await res.post.remove();
res.json({ message: 'Post deleted' });
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Add a comment to a post
router.post('/:id/comments', getPost, async (req, res) => {
const comment = {
body: req.body.body,
author: req.body.author
};
res.post.comments.push(comment);
try {
const updatedPost = await res.post.save();
res.status(201).json(updatedPost);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
// Middleware to get post by ID
async function getPost(req, res, next) {
let post;
try {
post = await Post.findById(req.params.id)
.populate('author', 'username email')
.populate('comments.author', 'username');
if (post == null) {
return res.status(404).json({ message: 'Post not found' });
}
} catch (err) {
return res.status(500).json({ message: err.message });
}
res.post = post;
next();
}
module.exports = router;
Step 3: Set Up Express App
const express = require('express');
const mongoose = require('mongoose');
const postsRouter = require('./routes/posts');
const usersRouter = require('./routes/users');
const app = express();
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/blog_api', {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true
});
const db = mongoose.connection;
db.on('error', (error) => console.error(error));
db.once('open', () => console.log('Connected to Database'));
// Middleware
app.use(express.json());
// Routes
app.use('/posts', postsRouter);
app.use('/users', usersRouter);
// Start the server
app.listen(3000, () => console.log('Server started on port 3000'));
Best Practices
When working with Mongoose and Express, follow these best practices:
- Organize your code: Separate models, routes, and controllers
- Error handling: Always use try/catch blocks and handle errors gracefully
- Validation: Use mongoose built-in validation to ensure data integrity
- Security: Never expose sensitive fields like passwords
- Indexing: Create indexes for frequently queried fields
- Lean queries: Use
.lean()
for read-only operations to improve performance - Pagination: Implement pagination for large collections
- Connection management: Handle connection errors and reconnection
Summary
In this tutorial, you've learned how to:
- Integrate Mongoose with Express
- Define schemas and models for your data
- Perform CRUD operations
- Use advanced Mongoose features like validation, middleware, and population
- Implement a real-world blog API
Mongoose provides a powerful abstraction layer on top of MongoDB that makes it easier to work with your data. By defining schemas, you get the benefits of validation, type casting, and query building while still maintaining the flexibility that MongoDB offers.
Additional Resources
- Official Mongoose Documentation
- MongoDB University - Free MongoDB courses
- Express.js Documentation
- MongoDB Node.js Driver
Exercises
- Create a RESTful API for a bookstore with models for books, authors, and categories
- Implement user authentication with password hashing using Mongoose middleware
- Add pagination and filtering to the blog API example
- Create a model with subdocuments and implement CRUD operations for them
- Build a search API with text indexes in MongoDB and Mongoose
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)