JavaScript 9 min read

MongoDB with Node.js: Complete Database Guide

Master MongoDB with Node.js and Mongoose. Learn CRUD operations, schema design, indexing, aggregation pipelines, and production best practices.

MR

Moshiour Rahman

Advertisement

What is MongoDB?

MongoDB is a document-oriented NoSQL database that stores data in flexible, JSON-like documents. It’s designed for scalability and developer productivity.

MongoDB vs SQL

SQL DatabaseMongoDB
TablesCollections
RowsDocuments
ColumnsFields
JoinsEmbedded documents / References
Schema requiredSchema optional

Getting Started

Setup

npm init -y
npm install mongodb mongoose
npm install -D typescript @types/node ts-node

Native MongoDB Driver

import { MongoClient, ObjectId } from 'mongodb';

const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri);

async function main() {
  try {
    await client.connect();
    console.log('Connected to MongoDB');

    const db = client.db('myapp');
    const users = db.collection('users');

    // Insert one
    const result = await users.insertOne({
      name: 'John Doe',
      email: 'john@example.com',
      createdAt: new Date()
    });
    console.log(`Inserted: ${result.insertedId}`);

    // Find one
    const user = await users.findOne({ email: 'john@example.com' });
    console.log(user);

  } finally {
    await client.close();
  }
}

main();

Mongoose ODM

Connection Setup

import mongoose from 'mongoose';

const connectDB = async () => {
  try {
    await mongoose.connect('mongodb://localhost:27017/myapp', {
      maxPoolSize: 10,
      serverSelectionTimeoutMS: 5000,
      socketTimeoutMS: 45000
    });
    console.log('MongoDB connected');
  } catch (error) {
    console.error('MongoDB connection error:', error);
    process.exit(1);
  }
};

// Handle connection events
mongoose.connection.on('disconnected', () => {
  console.log('MongoDB disconnected');
});

mongoose.connection.on('error', (err) => {
  console.error('MongoDB error:', err);
});

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

export default connectDB;

Schema Definition

import mongoose, { Schema, Document, Model } from 'mongoose';

// Interface for TypeScript
interface IUser extends Document {
  name: string;
  email: string;
  password: string;
  age?: number;
  role: 'user' | 'admin';
  profile: {
    bio?: string;
    avatar?: string;
    social: {
      twitter?: string;
      github?: string;
    };
  };
  posts: mongoose.Types.ObjectId[];
  createdAt: Date;
  updatedAt: Date;
}

// Schema definition
const userSchema = new Schema<IUser>({
  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,
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
  },
  password: {
    type: String,
    required: true,
    minlength: 8,
    select: false  // Don't include in queries by default
  },
  age: {
    type: Number,
    min: [0, 'Age cannot be negative'],
    max: [150, 'Invalid age']
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  profile: {
    bio: { type: String, maxlength: 500 },
    avatar: String,
    social: {
      twitter: String,
      github: String
    }
  },
  posts: [{
    type: Schema.Types.ObjectId,
    ref: 'Post'
  }]
}, {
  timestamps: true,  // Adds createdAt and updatedAt
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

// Indexes
userSchema.index({ email: 1 });
userSchema.index({ name: 'text', 'profile.bio': 'text' });

// Virtual field
userSchema.virtual('postCount').get(function() {
  return this.posts.length;
});

// Pre-save middleware
userSchema.pre('save', async function(next) {
  if (this.isModified('password')) {
    const bcrypt = require('bcryptjs');
    this.password = await bcrypt.hash(this.password, 12);
  }
  next();
});

// Instance methods
userSchema.methods.comparePassword = async function(candidatePassword: string) {
  const bcrypt = require('bcryptjs');
  return bcrypt.compare(candidatePassword, this.password);
};

// Static methods
userSchema.statics.findByEmail = function(email: string) {
  return this.findOne({ email });
};

const User: Model<IUser> = mongoose.model('User', userSchema);

export default User;

CRUD Operations

Create

// Create single document
const user = new User({
  name: 'John Doe',
  email: 'john@example.com',
  password: 'securepassword'
});
await user.save();

// Create with create()
const user2 = await User.create({
  name: 'Jane Doe',
  email: 'jane@example.com',
  password: 'securepassword'
});

// Create multiple
const users = await User.insertMany([
  { name: 'User 1', email: 'user1@example.com', password: 'pass1' },
  { name: 'User 2', email: 'user2@example.com', password: 'pass2' }
]);

Read

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

// Find with conditions
const activeAdmins = await User.find({
  role: 'admin',
  'profile.bio': { $exists: true }
});

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

// Find by ID
const userById = await User.findById('507f1f77bcf86cd799439011');

// Query operators
const users = await User.find({
  age: { $gte: 18, $lte: 65 },
  role: { $in: ['user', 'admin'] },
  name: { $regex: /john/i }
});

// Projection (select fields)
const usersProjected = await User.find()
  .select('name email -_id');

// Sort, limit, skip
const paginatedUsers = await User.find()
  .sort({ createdAt: -1 })
  .skip(10)
  .limit(10);

// Populate references
const userWithPosts = await User.findById(userId)
  .populate('posts', 'title content');

// Lean queries (plain objects, faster)
const leanUsers = await User.find().lean();

Update

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

// Update many
await User.updateMany(
  { role: 'user' },
  { $set: { 'profile.verified': false } }
);

// Find and update (returns document)
const updatedUser = await User.findOneAndUpdate(
  { email: 'john@example.com' },
  { $set: { name: 'John Updated' } },
  { new: true, runValidators: true }
);

// Find by ID and update
const user = await User.findByIdAndUpdate(
  userId,
  { $inc: { loginCount: 1 } },
  { new: true }
);

// Update operators
await User.updateOne({ _id: userId }, {
  $set: { name: 'New Name' },
  $unset: { temporaryField: '' },
  $push: { tags: 'new-tag' },
  $pull: { tags: 'old-tag' },
  $addToSet: { tags: 'unique-tag' },
  $inc: { viewCount: 1 }
});

Delete

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

// Delete many
await User.deleteMany({ role: 'guest' });

// Find and delete
const deletedUser = await User.findOneAndDelete({ email: 'john@example.com' });

// Find by ID and delete
const user = await User.findByIdAndDelete(userId);

Advanced Queries

Aggregation Pipeline

// Basic aggregation
const stats = await User.aggregate([
  // Match stage
  { $match: { role: 'user' } },

  // Group stage
  {
    $group: {
      _id: '$role',
      count: { $sum: 1 },
      avgAge: { $avg: '$age' },
      names: { $push: '$name' }
    }
  },

  // Sort stage
  { $sort: { count: -1 } }
]);

// Complex aggregation
const postStats = await Post.aggregate([
  // Lookup (join)
  {
    $lookup: {
      from: 'users',
      localField: 'author',
      foreignField: '_id',
      as: 'authorInfo'
    }
  },
  { $unwind: '$authorInfo' },

  // Project
  {
    $project: {
      title: 1,
      authorName: '$authorInfo.name',
      commentCount: { $size: '$comments' },
      createdAt: 1
    }
  },

  // Add fields
  {
    $addFields: {
      isPopular: { $gte: ['$commentCount', 10] }
    }
  },

  // Facet for multiple pipelines
  {
    $facet: {
      popular: [
        { $match: { isPopular: true } },
        { $limit: 5 }
      ],
      recent: [
        { $sort: { createdAt: -1 } },
        { $limit: 5 }
      ],
      stats: [
        {
          $group: {
            _id: null,
            totalPosts: { $sum: 1 },
            avgComments: { $avg: '$commentCount' }
          }
        }
      ]
    }
  }
]);
// Create text index
userSchema.index({ name: 'text', 'profile.bio': 'text' });

// Search
const results = await User.find(
  { $text: { $search: 'developer javascript' } },
  { score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });

Geospatial Queries

const locationSchema = new Schema({
  name: String,
  location: {
    type: { type: String, enum: ['Point'], default: 'Point' },
    coordinates: { type: [Number], required: true }  // [longitude, latitude]
  }
});

locationSchema.index({ location: '2dsphere' });

// Find nearby
const nearbyPlaces = await Location.find({
  location: {
    $near: {
      $geometry: {
        type: 'Point',
        coordinates: [-73.9857, 40.7484]  // NYC
      },
      $maxDistance: 5000  // meters
    }
  }
});

Relationships

Embedded Documents

// Good for: one-to-few, data always accessed together
const blogSchema = new Schema({
  title: String,
  content: String,
  comments: [{
    text: String,
    author: String,
    createdAt: { type: Date, default: Date.now }
  }]
});

// Add comment
await Blog.findByIdAndUpdate(blogId, {
  $push: { comments: { text: 'Great post!', author: 'John' } }
});

References

// Good for: one-to-many, data accessed separately
const postSchema = new Schema({
  title: String,
  author: { type: Schema.Types.ObjectId, ref: 'User' }
});

// Populate
const post = await Post.findById(postId).populate('author', 'name email');

// Deep populate
const post = await Post.findById(postId)
  .populate({
    path: 'author',
    populate: { path: 'friends', select: 'name' }
  });

Transactions

const session = await mongoose.startSession();

try {
  session.startTransaction();

  // Create user
  const user = await User.create([{ name: 'John', email: 'john@example.com' }], { session });

  // Create related post
  await Post.create([{ title: 'First Post', author: user[0]._id }], { session });

  // Update user
  await User.updateOne(
    { _id: user[0]._id },
    { $inc: { postCount: 1 } },
    { session }
  );

  await session.commitTransaction();
  console.log('Transaction committed');

} catch (error) {
  await session.abortTransaction();
  console.error('Transaction aborted:', error);
  throw error;

} finally {
  session.endSession();
}

Middleware (Hooks)

// Pre-save
userSchema.pre('save', function(next) {
  this.updatedAt = new Date();
  next();
});

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

// Pre-remove
userSchema.pre('remove', async function(next) {
  // Delete user's posts
  await Post.deleteMany({ author: this._id });
  next();
});

// Query middleware
userSchema.pre('find', function() {
  this.where({ isActive: true });
});

userSchema.pre('findOne', function() {
  this.populate('posts');
});

Performance

Indexing

// Single field index
userSchema.index({ email: 1 });

// Compound index
userSchema.index({ name: 1, createdAt: -1 });

// Unique index
userSchema.index({ email: 1 }, { unique: true });

// Sparse index (only index documents with field)
userSchema.index({ phone: 1 }, { sparse: true });

// TTL index (auto-delete)
sessionSchema.index({ createdAt: 1 }, { expireAfterSeconds: 3600 });

// Explain query
const explanation = await User.find({ email: 'john@example.com' }).explain('executionStats');

Query Optimization

// Use projection
const users = await User.find().select('name email');

// Use lean() for read-only
const users = await User.find().lean();

// Batch operations
await User.bulkWrite([
  { insertOne: { document: { name: 'User 1' } } },
  { updateOne: { filter: { _id: id1 }, update: { $set: { active: true } } } },
  { deleteOne: { filter: { _id: id2 } } }
]);

// Cursor for large datasets
const cursor = User.find().cursor();
for await (const user of cursor) {
  console.log(user.name);
}

Summary

OperationMethod
Createsave(), create(), insertMany()
Readfind(), findOne(), findById()
UpdateupdateOne(), findByIdAndUpdate()
DeletedeleteOne(), findByIdAndDelete()
Aggregateaggregate()
Populatepopulate()

MongoDB with Mongoose provides a powerful, flexible solution for Node.js applications requiring document-based data storage.

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.