Back to all posts

MongoDB using Mongoose


Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It provides a schema-based solution to model your application data and includes built-in type casting validation, query building, business logic hooks and more.

How to setup

To get started with Mongoose, You’ill need to install both MongoDB and the Mongoose package:

npm install mongoose

How to connect

const mongoose = require('mongoose');

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/mydatabase', {
  useNewUrlParser: true,
  useUnifiedTopology: true
})
  .then(() => console.log('MongoDB connected successfully'))
  .catch(err => console.error('MongoDB connection error:', err));

// Alternative connection with async/await
async function connectDB() {
  try {
    await mongoose.connect('mongodb://localhost:27017/mydatabase');
    console.log('MongoDB connected successfully');
  } catch (error) {
    console.error('MongoDB connection error:', error);
    process.exit(1);
  }
}

How to create Schemas

A Schema defines the structure of your document within a collection.

const mongoose = require('mongoose');
const { Schema } = mongoose;

// Create a Schema
const userSchema = new Schema({
  name: {
    type: String,
    required: true,
    trim: true
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true
  },
  password: {
    type: String,
    required: true,
    minlength: 6
  },
  age: {
    type: Number,
    min: 18,
    default: 18
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  isActive: {
    type: Boolean,
    default: true
  },
  tags: [String],
  address: {
    street: String,
    city: String,
    state: String,
    zipCode: String
  }
});

Creating Models

Models are fancy constructors compiled form Schema definitions. They are responsible for creating and reading documents from the MongoDB database.

// Create a model from the schema
const User = mongoose.model(‘User’, userSchema);

// Export the model
module.exports = User;

Let do CRUD Operations

Create

// Method 1: Create and save
const user = new User({
  name: 'John Doe',
  email: 'john@example.com',
  password: 'password123',
  age: 30,
  tags: ['developer', 'node.js'],
  address: {
    street: '123 Main St',
    city: 'New York',
    state: 'NY',
    zipCode: '10001'
  }
});

// Save the user
await user.save();

// Method 2: Create method
const user = await User.create({
  name: 'Jane Smith',
  email: 'jane@example.com',
  password: 'secure123',
  age: 25
});

Read

// Find all users
const users = await User.find();

// Find by ID
const user = await User.findById('60d21b4667d0d8992e610c85');

// Find one document that matches criteria
const user = await User.findOne({ email: 'john@example.com' });

// Find with specific fields
const users = await User.find({}, 'name email'); // Only return name and email

// Find with conditions
const activeUsers = await User.find({ isActive: true, age: { $gte: 21 } });

// Pagination
const page = 1;
const limit = 10;
const users = await User.find()
  .skip((page - 1) * limit)
  .limit(limit);

// Sorting
const users = await User.find().sort({ name: 1 }); // 1 for ascending, -1 for descending

Update

// Find and update by ID
const updatedUser = await User.findByIdAndUpdate(
  '60d21b4667d0d8992e610c85',
  { name: 'Updated Name' },
  { new: true } // Return the updated document
);

// Update one document
const result = await User.updateOne(
  { email: 'john@example.com' },
  { $set: { isActive: false } }
);

// Update many documents
const result = await User.updateMany(
  { age: { $lt: 21 } },
  { $set: { isActive: false } }
);

Delete

// Find and delete by ID
const deletedUser = await User.findByIdAndDelete('60d21b4667d0d8992e610c85');

// Delete one document
const result = await User.deleteOne({ email: 'john@example.com' });

// Delete many documents
const result = await User.deleteMany({ isActive: false });

Validation

Mongoose provides built-in and custom validators to ensure your data meets specific requirements.

const userSchema = new Schema({
  email: {
    type: String,
    required: [true, 'Email is required'],
    unique: true,
    validate: {
      validator: function(v) {
        return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(v);
      },
      message: props => `${props.value} is not a valid email!`
    }
  },
  website: {
    type: String,
    validate: {
      validator: function(v) {
        return /^(http|https):\/\/[^ "]+$/.test(v);
      },
      message: props => `${props.value} is not a valid URL!`
    }
  },
  phone: {
    type: String,
    validate: {
      validator: function(v) {
        return /\d{3}-\d{3}-\d{4}/.test(v);
      },
      message: props => `${props.value} is not a valid phone number!`
    }
  }
});

Middleware

Mongoose provides middleware (pre and post hooks) for controlling the execution flow.

// Hash password before saving
const bcrypt = require('bcrypt');

userSchema.pre('save', async function(next) {
  const user = this;
  
  // Only hash the password if it's modified or new
  if (!user.isModified('password')) return next();
  
  try {
    // Generate salt
    const salt = await bcrypt.genSalt(10);
    // Hash password
    user.password = await bcrypt.hash(user.password, salt);
    next();
  } catch (error) {
    next(error);
  }
});

// Add a method to compare passwords
userSchema.methods.comparePassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

// Pre find hook - Example: always exclude inactive users
userSchema.pre('find', function() {
  this.where({ isActive: true });
});

// Post save hook
userSchema.post('save', function(doc, next) {
  console.log(`User ${doc.name} has been saved`);
  next();
});

Relationships

References (Normalization)

// User Schema with references to posts
const userSchema = new Schema({
  name: String,
  email: String,
  posts: [{
    type: Schema.Types.ObjectId,
    ref: 'Post'
  }]
});

// Post Schema
const postSchema = new Schema({
  title: String,
  content: String,
  author: {
    type: Schema.Types.ObjectId,
    ref: 'User',
    required: true
  }
});

const User = mongoose.model('User', userSchema);
const Post = mongoose.model('Post', postSchema);

// Creating related documents
const user = new User({ name: 'John', email: 'john@example.com' });
await user.save();

const post = new Post({
  title: 'My first post',
  content: 'Hello world!',
  author: user._id
});
await post.save();

// Update user's posts array
user.posts.push(post._id);
await user.save();

// Populate references
const userWithPosts = await User.findById(user._id).populate('posts');
const postWithAuthor = await Post.findById(post._id).populate('author');

Embedded Documents (Denormalization)

const commentSchema = new Schema({
  text: String,
  createdAt: {
    type: Date,
    default: Date.now
  },
  user: {
    name: String,
    email: String
  }
});

const postSchema = new Schema({
  title: String,
  content: String,
  comments: [commentSchema]
});

const Post = mongoose.model('Post', postSchema);

// Create a post with embedded comments
const post = new Post({
  title: 'Embedded Documents Demo',
  content: 'This post demonstrates embedded documents',
  comments: [
    {
      text: 'Great post!',
      user: { name: 'Alice', email: 'alice@example.com' }
    },
    {
      text: 'Thanks for sharing',
      user: { name: 'Bob', email: 'bob@example.com' }
    }
  ]
});

await post.save();

// Add a new comment
post.comments.push({
  text: 'I agree with Alice',
  user: { name: 'Charlie', email: 'charlie@example.com' }
});

await post.save();

Advanced Queries

Query Operators

// Comparison operators
const users = await User.find({
  age: { $gt: 18, $lt: 65 }, // Greater than 18 and less than 65
  tags: { $in: ['developer', 'designer'] } // Tags include either 'developer' or 'designer'
});

// Logical operators
const users = await User.find({
  $or: [
    { age: { $lt: 18 } },
    { age: { $gt: 65 } }
  ],
  $and: [
    { isActive: true },
    { email: { $exists: true } }
  ]
});

// Element operators
const users = await User.find({
  website: { $exists: true }, // Field exists
  address: { $type: 'object' } // Field is an object
});

// Array operators
const users = await User.find({
  tags: { $all: ['developer', 'node.js'] }, // Contains all specified elements
  'address.zipCode': { $regex: /^100/ } // Regex match on nested field
});

Aggregation Pipeline

const result = await User.aggregate([
  // Stage 1: Match users over 18
  { $match: { age: { $gt: 18 } } },
  
  // Stage 2: Group by state and count
  { $group: {
    _id: '$address.state',
    count: { $sum: 1 },
    avgAge: { $avg: '$age' }
  }},
  
  // Stage 3: Sort by count descending
  { $sort: { count: -1 } },
  
  // Stage 4: Limit to top 5
  { $limit: 5 },
  
  // Stage 5: Project to rename fields
  { $project: {
    state: '$_id',
    count: 1,
    avgAge: 1,
    _id: 0
  }}
]);

Best Practices

1 Always handle errors: Use try/catch blocks or .catch() with promises.

2 Create indexes for frequently queried fields:

userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ name: 'text' }); // Text index for searching

3. Use lean queries for better performance when you don’t need Mongoose documents:

const users = await User.find().lean();

4. Limit returned fields to only what you need:

const users = await User.find({}, 'name email -_id');

5. Use appropriate validation to ensure data integrity.

6. Consider schema design carefully:

  • Embed data when it’s always accessed together with the parent
  • Reference data when it needs to be accessed independently
  • Consider the document size limit (16MB)

7. Use transactions for operations that need to be atomic:

const session = await mongoose.startSession();
session.startTransaction();

try {
  // Operations that need to be atomic
  const user = await User.create([{ name: 'John' }], { session });
  await Post.create([{ title: 'Post', author: user[0]._id }], { session });
  
  await session.commitTransaction();
} catch (error) {
  await session.abortTransaction();
  throw error;
} finally {
  session.endSession();
}

8. Implement pagination for large collections.

9. Close the connection when your app terminates:

process.on('SIGINT', async () => {
  await mongoose.connection.close();
  process.exit(0);
});