Skip to main content

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:

bash
npm install express mongoose

Connecting to MongoDB

Create your Express application and establish a MongoDB connection:

javascript
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 one
  • useUnifiedTopology: Use the new Server Discovery and Monitoring engine
  • useCreateIndex: 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.

javascript
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:

javascript
// 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)

javascript
// 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:

json
POST /users
Content-Type: application/json

{
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"age": 30
}

Example Response:

json
{
"_id": "60a1c2b9b5f7f83e8c9d1234",
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"age": 30,
"isActive": true,
"createdAt": "2023-07-15T12:34:56.789Z",
"__v": 0
}

Read (GET)

javascript
// 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)

javascript
// 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:

json
PUT /users/60a1c2b9b5f7f83e8c9d1234
Content-Type: application/json

{
"firstName": "Jonathan",
"age": 31
}

Example Response:

json
{
"_id": "60a1c2b9b5f7f83e8c9d1234",
"firstName": "Jonathan",
"lastName": "Doe",
"email": "[email protected]",
"age": 31,
"isActive": true,
"createdAt": "2023-07-15T12:34:56.789Z",
"__v": 0
}

Delete (DELETE)

javascript
// 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:

javascript
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:

javascript
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:

javascript
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:

javascript
// 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:

javascript
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

javascript
// 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

javascript
// 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

javascript
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:

  1. Organize your code: Separate models, routes, and controllers
  2. Error handling: Always use try/catch blocks and handle errors gracefully
  3. Validation: Use mongoose built-in validation to ensure data integrity
  4. Security: Never expose sensitive fields like passwords
  5. Indexing: Create indexes for frequently queried fields
  6. Lean queries: Use .lean() for read-only operations to improve performance
  7. Pagination: Implement pagination for large collections
  8. 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

Exercises

  1. Create a RESTful API for a bookstore with models for books, authors, and categories
  2. Implement user authentication with password hashing using Mongoose middleware
  3. Add pagination and filtering to the blog API example
  4. Create a model with subdocuments and implement CRUD operations for them
  5. 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! :)