Understanding MongoDB in the NoSQL Landscape
MongoDB is a popular document-oriented NoSQL database designed for scalability, flexibility, and performance. Unlike relational databases that store data in tables with rigid schemas, MongoDB stores data in flexible, JSON-like documents where fields can vary from document to document.
Key MongoDB Concepts
Document
The basic unit of data in MongoDB, similar to a row in a relational database but with a flexible schema. Documents are stored in BSON (Binary JSON) format.
{
"_id": ObjectId("60a91cd3b3e55a0015c0e980"),
"name": "John Smith",
"email": "john@example.com",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
},
"interests": ["programming", "hiking", "reading"]
}
Collection
A grouping of MongoDB documents, analogous to a table in relational databases. Collections do not enforce schema on documents.
Database
A container for collections, similar to a database in relational systems. A single MongoDB server can host multiple databases.
Field
A key-value pair within a document. Fields can contain various data types, including other documents and arrays.
_id
A unique identifier automatically added to each document, functioning as a primary key. Defaults to an ObjectId, but can be overridden.
MongoDB vs. Relational Databases
| Relational (e.g., PostgreSQL) | MongoDB |
|---|---|
| Tables | Collections |
| Rows | Documents |
| Columns | Fields |
| Joins | Embedded Documents & References |
| Fixed Schema | Dynamic Schema |
| SQL | JSON-based Query Language |
| ACID Transactions (full) | ACID Transactions (since v4.0, with limitations) |
When to Choose MongoDB
MongoDB excels in specific scenarios:
- Rapid Development: When you need to iterate quickly and your schema may evolve
- Semi-structured or Unstructured Data: When data doesn't fit neatly into tabular format
- Hierarchical Data Storage: When you have nested, complex data structures
- High Write Load: For applications with heavy write operations
- Horizontal Scaling: When you need to scale across multiple servers easily
- Content Management: For storing content with varying attributes
- Real-time Analytics: For applications requiring fast, simple analytics
- Prototyping: For quickly building MVPs or proof of concepts
Real-World Examples: Content management systems, mobile apps, IoT applications, gaming applications, event logging, and real-time analytics.
Installing and Setting Up MongoDB
Let's start by installing MongoDB on your development machine. MongoDB offers Community Edition (free) and Enterprise Edition (commercial, with additional features).
Windows Installation
- Download the MongoDB Community Server installer from MongoDB Download Center
- Run the MSI installer and follow the setup wizard
- Choose "Complete" installation type
- Optionally install MongoDB Compass (GUI tool)
- MongoDB will be installed as a Windows service by default
macOS Installation
Using Homebrew (Recommended)
# Install Homebrew if you don't already have it
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install MongoDB Community Edition
brew tap mongodb/brew
brew install mongodb-community
# Start MongoDB service
brew services start mongodb-community
Linux Installation (Ubuntu)
# Import MongoDB public GPG key
wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -
# Create a list file for MongoDB
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list
# Update package database
sudo apt-get update
# Install MongoDB packages
sudo apt-get install -y mongodb-org
# Start MongoDB service
sudo systemctl start mongod
# Enable MongoDB to start on boot
sudo systemctl enable mongod
Docker Installation
Using Docker is a clean, cross-platform way to run MongoDB:
# Pull the MongoDB image
docker pull mongo
# Run MongoDB container
docker run --name mongodb -d -p 27017:27017 mongo
# To run with authentication enabled
docker run --name mongodb -d -p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=password \
mongo
MongoDB Atlas: Cloud-Hosted Option
MongoDB Atlas is the official fully-managed cloud database service. It's an excellent option for development and production deployments without the overhead of managing infrastructure.
- Go to MongoDB Atlas and create an account
- Create a new project
- Build a cluster (the free tier is sufficient for development)
- Configure database access (create a user with password)
- Configure network access (whitelist your IP address)
- Get your connection string from the "Connect" button
⚠️ Security Note
For production use, always secure your MongoDB installation:
- Enable authentication
- Configure network security (firewalls)
- Use TLS/SSL for encrypted connections
- Follow the principle of least privilege for user permissions
MongoDB Compass: GUI Tool
MongoDB Compass is the official GUI for MongoDB, providing a visual interface to explore and manipulate your data.
- Download MongoDB Compass from MongoDB Compass Download
- Install and open the application
- Connect to your MongoDB instance using the connection string
MongoDB CRUD Operations
Let's explore the basic CRUD (Create, Read, Update, Delete) operations in MongoDB using the MongoDB Shell, a command-line interface for interacting with MongoDB.
Accessing the MongoDB Shell
# Connect to local MongoDB instance
mongosh
# Connect to a specific database
mongosh mydatabase
# Connect with authentication
mongosh --username myuser --password mypassword --authenticationDatabase admin
Creating and Switching Databases
// Show all databases
show dbs
// Switch to a database (creates it if it doesn't exist)
use ecommerce
// Check current database
db
Creating Collections
// Explicitly create a collection
db.createCollection("products")
// Show all collections
show collections
// Collections are also created implicitly when you insert documents
db.users.insertOne({ name: "John", email: "john@example.com" })
Create Operations
// Insert a single document
db.products.insertOne({
name: "Smartphone",
price: 699.99,
category: "Electronics",
specs: {
brand: "TechCo",
model: "X1",
storage: "128GB",
color: "Black"
},
inStock: true,
tags: ["smartphone", "tech", "mobile"]
})
// Insert multiple documents
db.products.insertMany([
{
name: "Laptop",
price: 1299.99,
category: "Electronics",
specs: {
brand: "TechCo",
model: "UltraBook",
processor: "Core i7",
ram: "16GB",
storage: "512GB SSD"
},
inStock: true,
tags: ["laptop", "computer", "tech"]
},
{
name: "Headphones",
price: 199.99,
category: "Electronics",
specs: {
brand: "AudioPro",
model: "SoundMax",
type: "Over-ear",
wireless: true
},
inStock: false,
tags: ["audio", "headphones", "wireless"]
}
])
Read Operations
// Find all documents in a collection
db.products.find()
// Pretty-print results
db.products.find().pretty()
// Find with a simple filter
db.products.find({ category: "Electronics" })
// Find with multiple conditions
db.products.find({
category: "Electronics",
price: { $lt: 1000 },
inStock: true
})
// Find with nested fields
db.products.find({ "specs.brand": "TechCo" })
// Find with array conditions
db.products.find({ tags: "wireless" })
// Find one document
db.products.findOne({ name: "Laptop" })
// Find with projection (selecting fields)
db.products.find(
{ category: "Electronics" },
{ name: 1, price: 1, _id: 0 } // 1 to include, 0 to exclude
)
Query Operators
// Comparison operators
db.products.find({ price: { $gt: 500 } }) // Greater than
db.products.find({ price: { $gte: 500 } }) // Greater than or equal
db.products.find({ price: { $lt: 300 } }) // Less than
db.products.find({ price: { $lte: 300 } }) // Less than or equal
db.products.find({ price: { $ne: 699.99 } }) // Not equal
db.products.find({ price: { $in: [199.99, 699.99] } }) // In array
db.products.find({ price: { $nin: [199.99, 699.99] } }) // Not in array
// Logical operators
db.products.find({
$and: [
{ price: { $gt: 500 } },
{ inStock: true }
]
})
db.products.find({
$or: [
{ price: { $lt: 200 } },
{ price: { $gt: 1000 } }
]
})
db.products.find({
price: { $not: { $gt: 1000 } }
})
// Element operators
db.products.find({ specs: { $exists: true } })
db.products.find({ price: { $type: "number" } })
// Array operators
db.products.find({ tags: { $all: ["tech", "mobile"] } })
db.products.find({ tags: { $size: 3 } })
Sorting, Limiting, and Skipping
// Sort results (1 for ascending, -1 for descending)
db.products.find().sort({ price: 1 })
// Limit results
db.products.find().limit(2)
// Skip results (for pagination)
db.products.find().skip(1).limit(2)
// Combining methods
db.products.find({ category: "Electronics" })
.sort({ price: -1 })
.skip(1)
.limit(2)
Update Operations
// Update a single document
db.products.updateOne(
{ name: "Smartphone" },
{ $set: { price: 649.99, "specs.color": "Blue" } }
)
// Update multiple documents
db.products.updateMany(
{ category: "Electronics" },
{ $set: { onSale: true } }
)
// Increment values
db.products.updateOne(
{ name: "Laptop" },
{ $inc: { price: 100 } }
)
// Add to arrays
db.products.updateOne(
{ name: "Headphones" },
{ $push: { tags: "premium" } }
)
// Remove from arrays
db.products.updateOne(
{ name: "Headphones" },
{ $pull: { tags: "wireless" } }
)
// Update or insert (upsert)
db.products.updateOne(
{ name: "Tablet" },
{
$set: {
price: 399.99,
category: "Electronics",
inStock: true
}
},
{ upsert: true }
)
// Replace a document
db.products.replaceOne(
{ name: "Smartphone" },
{
name: "Smartphone",
price: 799.99,
category: "Electronics",
specs: {
brand: "TechCo",
model: "X2",
storage: "256GB",
color: "Silver"
},
inStock: true,
tags: ["smartphone", "tech", "mobile", "flagship"]
}
)
Delete Operations
// Delete a single document
db.products.deleteOne({ name: "Headphones" })
// Delete multiple documents
db.products.deleteMany({ inStock: false })
// Delete all documents in a collection
db.products.deleteMany({})
// Drop a collection
db.products.drop()
// Drop a database
db.dropDatabase()
Data Modeling in MongoDB
Data modeling in MongoDB is fundamentally different from relational databases. Instead of normalizing data across tables, MongoDB encourages embedding related data in a single document when appropriate.
Key Data Modeling Concepts
- Embedding vs. Referencing: The two main approaches to representing relationships
- Schema Design Patterns: Common patterns for solving specific data modeling challenges
- Document Growth: Considering how documents might grow over time
- Atomicity: Operations on a single document are atomic
- Read vs. Write Optimization: Balancing data model for different access patterns
Embedding vs. Referencing
Embedding (Denormalized)
Include related data within the same document:
// User document with embedded addresses
{
"_id": ObjectId("..."),
"name": "John Smith",
"email": "john@example.com",
"addresses": [
{
"type": "home",
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
},
{
"type": "work",
"street": "456 Business Ave",
"city": "Workville",
"state": "CA",
"zip": "67890"
}
]
}
Advantages:
- Retrieves all related data in a single query
- Better performance for read operations
- Maintains data locality
- Atomic updates
Disadvantages:
- Documents can grow larger over time
- Can't access embedded data independently
- Duplication if data is needed in multiple places
When to use: For "contains" relationships, when data is always accessed together, for smaller data sets, for one-to-few relationships.
Referencing (Normalized)
Store references to documents in other collections:
// User document with references to addresses
{
"_id": ObjectId("..."),
"name": "John Smith",
"email": "john@example.com",
"address_ids": [
ObjectId("address1..."),
ObjectId("address2...")
]
}
// Address collection
{
"_id": ObjectId("address1..."),
"user_id": ObjectId("..."),
"type": "home",
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
}
Advantages:
- Avoids data duplication
- Keeps documents smaller
- Better for many-to-many relationships
- Allows independent access to referenced data
Disadvantages:
- Requires multiple queries or complex aggregations
- No atomic updates across multiple documents
- Can be slower for reads
When to use: For one-to-many or many-to-many relationships, when data is frequently updated, for larger data sets, when data is accessed independently.
Common Data Modeling Patterns
1. One-to-Few: Embedded Documents
// Product with a few variants
{
"_id": ObjectId("..."),
"name": "T-Shirt",
"base_price": 19.99,
"variants": [
{
"size": "S",
"color": "Blue",
"price": 19.99,
"stock": 25
},
{
"size": "M",
"color": "Blue",
"price": 19.99,
"stock": 30
},
{
"size": "L",
"color": "Blue",
"price": 22.99,
"stock": 15
}
]
}
2. One-to-Many: Array of References
// Author with many books
{
"_id": ObjectId("author1..."),
"name": "Jane Author",
"bio": "Bestselling author of...",
"book_ids": [
ObjectId("book1..."),
ObjectId("book2..."),
ObjectId("book3...")
]
}
// Books collection
{
"_id": ObjectId("book1..."),
"title": "My First Book",
"author_id": ObjectId("author1..."),
"published_date": ISODate("2020-01-15"),
"pages": 320
}
3. One-to-Squillions: References in the "Many" Side
// User document (no references to logs)
{
"_id": ObjectId("user1..."),
"username": "jsmith",
"email": "john@example.com"
}
// Log entries with references to user
{
"_id": ObjectId("log1..."),
"user_id": ObjectId("user1..."),
"action": "login",
"timestamp": ISODate("2023-04-15T08:30:00Z"),
"ip": "192.168.1.1"
}
4. Many-to-Many: Array of References on Both Sides
// Student with references to courses
{
"_id": ObjectId("student1..."),
"name": "Alice Student",
"course_ids": [
ObjectId("course1..."),
ObjectId("course2...")
]
}
// Course with references to students
{
"_id": ObjectId("course1..."),
"title": "Database Design",
"student_ids": [
ObjectId("student1..."),
ObjectId("student2..."),
ObjectId("student3...")
]
}
5. Subset Pattern: Embedding a Subset of Information
// Product with most important information embedded
{
"_id": ObjectId("order1..."),
"user_id": ObjectId("user1..."),
"date": ISODate("2023-04-15"),
"status": "shipped",
"items": [
{
"product_id": ObjectId("product1..."),
"product_name": "Smartphone X1", // Embed essential product info
"price": 699.99,
"quantity": 1
},
{
"product_id": ObjectId("product2..."),
"product_name": "Wireless Headphones",
"price": 199.99,
"quantity": 1
}
],
"total": 899.98
}
Schema Validation
MongoDB allows you to enforce a schema within a collection through validation rules:
db.createCollection("products", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "price", "category"],
properties: {
name: {
bsonType: "string",
description: "must be a string and is required"
},
price: {
bsonType: "number",
minimum: 0,
description: "must be a non-negative number and is required"
},
category: {
bsonType: "string",
description: "must be a string and is required"
},
description: {
bsonType: "string",
description: "must be a string if the field exists"
},
tags: {
bsonType: "array",
items: {
bsonType: "string"
}
}
}
}
},
validationLevel: "moderate",
validationAction: "error"
})
MongoDB with Node.js
Now let's learn how to integrate MongoDB with a Node.js application using Mongoose, a popular ODM (Object Data Modeling) library that provides a straightforward, schema-based solution.
Setting Up a Node.js Project with MongoDB
# Create project directory
mkdir mongodb-node-demo
cd mongodb-node-demo
# Initialize a new Node.js project
npm init -y
# Install required packages
npm install express mongoose dotenv
npm install --save-dev nodemon
Creating a Basic Express App with MongoDB Connection
First, create a .env file to store your MongoDB connection string:
# .env file
MONGODB_URI=mongodb://localhost:27017/mydatabase
PORT=3000
Now, let's create the main server file:
// server.js
const express = require('express');
const mongoose = require('mongoose');
require('dotenv').config();
// Initialize Express app
const app = express();
app.use(express.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI)
.then(() => console.log('Connected to MongoDB'))
.catch(err => {
console.error('Failed to connect to MongoDB', err);
process.exit(1);
});
// Basic route
app.get('/', (req, res) => {
res.send('MongoDB Node.js API is running');
});
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Defining Mongoose Schemas and Models
Mongoose allows you to define schemas for your MongoDB collections, providing structure and validation.
// models/Product.js
const mongoose = require('mongoose');
// Define a schema
const productSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
price: {
type: Number,
required: true,
min: 0
},
description: {
type: String,
trim: true
},
category: {
type: String,
required: true,
enum: ['Electronics', 'Clothing', 'Books', 'Home', 'Sports']
},
inStock: {
type: Boolean,
default: true
},
specs: {
type: Map,
of: String
},
tags: [String],
createdAt: {
type: Date,
default: Date.now
},
updatedAt: Date
});
// Add a pre-save hook
productSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
// Create a model
const Product = mongoose.model('Product', productSchema);
module.exports = Product;
Building RESTful API Routes
// routes/productRoutes.js
const express = require('express');
const router = express.Router();
const Product = require('../models/Product');
// GET all products
router.get('/', async (req, res) => {
try {
const products = await Product.find();
res.json(products);
} catch (err) {
console.error('Error fetching products:', err);
res.status(500).json({ message: 'Server error' });
}
});
// GET a single product
router.get('/:id', async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.json(product);
} catch (err) {
console.error('Error fetching product:', err);
if (err.kind === 'ObjectId') {
return res.status(400).json({ message: 'Invalid ID format' });
}
res.status(500).json({ message: 'Server error' });
}
});
// POST a new product
router.post('/', async (req, res) => {
try {
const newProduct = new Product(req.body);
const savedProduct = await newProduct.save();
res.status(201).json(savedProduct);
} catch (err) {
console.error('Error creating product:', err);
if (err.name === 'ValidationError') {
return res.status(400).json({
message: 'Validation Error',
errors: Object.values(err.errors).map(e => e.message)
});
}
res.status(500).json({ message: 'Server error' });
}
});
// PUT (update) a product
router.put('/:id', async (req, res) => {
try {
const updatedProduct = await Product.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!updatedProduct) {
return res.status(404).json({ message: 'Product not found' });
}
res.json(updatedProduct);
} catch (err) {
console.error('Error updating product:', err);
if (err.kind === 'ObjectId') {
return res.status(400).json({ message: 'Invalid ID format' });
}
if (err.name === 'ValidationError') {
return res.status(400).json({
message: 'Validation Error',
errors: Object.values(err.errors).map(e => e.message)
});
}
res.status(500).json({ message: 'Server error' });
}
});
// DELETE a product
router.delete('/:id', async (req, res) => {
try {
const deletedProduct = await Product.findByIdAndDelete(req.params.id);
if (!deletedProduct) {
return res.status(404).json({ message: 'Product not found' });
}
res.json({ message: 'Product deleted successfully' });
} catch (err) {
console.error('Error deleting product:', err);
if (err.kind === 'ObjectId') {
return res.status(400).json({ message: 'Invalid ID format' });
}
res.status(500).json({ message: 'Server error' });
}
});
module.exports = router;
Update your server.js to use the routes:
// server.js (updated)
// ... existing code ...
// Import routes
const productRoutes = require('./routes/productRoutes');
// Use routes
app.use('/api/products', productRoutes);
// ... rest of the code ...
MongoDB Relationships with Mongoose
Let's implement a relationship between Users and Orders in our e-commerce example:
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
// Virtual reference to orders
});
// Add a virtual field for orders
userSchema.virtual('orders', {
ref: 'Order',
localField: '_id',
foreignField: 'user'
});
// Set virtuals in JSON
userSchema.set('toJSON', { virtuals: true });
userSchema.set('toObject', { virtuals: true });
const User = mongoose.model('User', userSchema);
module.exports = User;
// models/Order.js
const mongoose = require('mongoose');
const orderItemSchema = new mongoose.Schema({
product: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true
},
name: String,
quantity: {
type: Number,
required: true,
min: 1
},
price: {
type: Number,
required: true
}
});
const orderSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
items: [orderItemSchema],
total: {
type: Number,
required: true
},
status: {
type: String,
enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'],
default: 'pending'
},
createdAt: {
type: Date,
default: Date.now
}
});
const Order = mongoose.model('Order', orderSchema);
module.exports = Order;
Using Population to Fetch Related Data
// Get orders for a user with populated product details
router.get('/users/:userId/orders', async (req, res) => {
try {
const orders = await Order.find({ user: req.params.userId })
.populate({
path: 'items.product',
select: 'name category'
})
.sort({ createdAt: -1 });
res.json(orders);
} catch (err) {
console.error('Error fetching orders:', err);
res.status(500).json({ message: 'Server error' });
}
});
// Get a user with their orders
router.get('/users/:id/with-orders', async (req, res) => {
try {
const user = await User.findById(req.params.id)
.populate({
path: 'orders',
options: { sort: { createdAt: -1 } }
});
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.json(user);
} catch (err) {
console.error('Error fetching user with orders:', err);
res.status(500).json({ message: 'Server error' });
}
});
Advanced Mongoose and MongoDB Features
Query Building and Middleware
// Query Building
const getProductsByCategory = async (category, options = {}) => {
const query = Product.find({ category });
// Apply filters
if (options.minPrice !== undefined) {
query.where('price').gte(options.minPrice);
}
if (options.maxPrice !== undefined) {
query.where('price').lte(options.maxPrice);
}
if (options.inStock !== undefined) {
query.where('inStock').equals(options.inStock);
}
// Apply sorting
if (options.sortBy) {
const sortOrder = options.sortDir === 'desc' ? -1 : 1;
query.sort({ [options.sortBy]: sortOrder });
} else {
query.sort({ createdAt: -1 });
}
// Apply pagination
const page = options.page || 1;
const limit = options.limit || 10;
const skip = (page - 1) * limit;
query.skip(skip).limit(limit);
// Execute query
return query.exec();
};
// Query Middleware (Hooks)
// Add this to the productSchema
productSchema.pre('find', function() {
console.log('Executing find operation on products');
this.startTime = Date.now();
});
productSchema.post('find', function(result) {
console.log(`Find operation completed in ${Date.now() - this.startTime}ms`);
});
// Static method on schema
productSchema.statics.findByCategory = function(category) {
return this.find({ category });
};
// Instance method on schema
productSchema.methods.applyDiscount = function(percentage) {
this.price = this.price * (1 - percentage / 100);
return this.save();
};
// Using the methods
const electronicsProducts = await Product.findByCategory('Electronics');
const product = await Product.findById(productId);
await product.applyDiscount(10); // Apply 10% discount
Aggregation Framework
MongoDB's aggregation framework is a powerful tool for processing and analyzing data within the database.
// Calculate sales by category
router.get('/sales-by-category', async (req, res) => {
try {
const salesByCategory = await Order.aggregate([
// Only include completed orders
{ $match: { status: { $in: ['shipped', 'delivered'] } } },
// Unwind the items array
{ $unwind: '$items' },
// Lookup to get product details
{
$lookup: {
from: 'products',
localField: 'items.product',
foreignField: '_id',
as: 'productDetails'
}
},
// Unwind the product details
{ $unwind: '$productDetails' },
// Group by category and sum sales
{
$group: {
_id: '$productDetails.category',
totalSales: { $sum: { $multiply: ['$items.quantity', '$items.price'] } },
count: { $sum: 1 }
}
},
// Sort by total sales (descending)
{ $sort: { totalSales: -1 } },
// Project to rename fields
{
$project: {
category: '$_id',
totalSales: 1,
count: 1,
_id: 0
}
}
]);
res.json(salesByCategory);
} catch (err) {
console.error('Error aggregating sales data:', err);
res.status(500).json({ message: 'Server error' });
}
});
Transactions
MongoDB supports multi-document transactions, allowing for atomic operations across multiple documents and collections.
// Function to process an order
const processOrder = async (userId, items) => {
// Start a session
const session = await mongoose.startSession();
session.startTransaction();
try {
// Calculate total price and create order items
let total = 0;
const orderItems = [];
// Process each item
for (const item of items) {
// Find the product and ensure it's in stock
const product = await Product.findById(item.productId).session(session);
if (!product) {
throw new Error(`Product ${item.productId} not found`);
}
if (!product.inStock) {
throw new Error(`Product ${product.name} is out of stock`);
}
// Create order item and add to total
const orderItem = {
product: product._id,
name: product.name,
quantity: item.quantity,
price: product.price
};
orderItems.push(orderItem);
total += product.price * item.quantity;
// Update product stock
await Product.findByIdAndUpdate(
product._id,
{ inStock: product.quantity > 1 },
{ session }
);
}
// Create the order
const order = new Order({
user: userId,
items: orderItems,
total
});
await order.save({ session });
// Commit the transaction
await session.commitTransaction();
session.endSession();
return order;
} catch (error) {
// If an error occurred, abort the transaction
await session.abortTransaction();
session.endSession();
throw error;
}
};
Indexing for Performance
// Define indexes in Mongoose schema
const productSchema = new mongoose.Schema({
// ... fields ...
});
// Simple index
productSchema.index({ name: 1 });
// Compound index
productSchema.index({ category: 1, price: -1 });
// Text index for search
productSchema.index(
{ name: 'text', description: 'text' },
{ weights: { name: 10, description: 5 } }
);
// Using text search
router.get('/search', async (req, res) => {
try {
const { q } = req.query;
if (!q) {
return res.status(400).json({ message: 'Search query is required' });
}
const products = await Product.find(
{ $text: { $search: q } },
{ score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });
res.json(products);
} catch (err) {
console.error('Search error:', err);
res.status(500).json({ message: 'Server error' });
}
});
Practical Activities
Activity 1: Setting Up a MongoDB Database
- Install MongoDB locally or set up a MongoDB Atlas free tier cluster
- Connect to your MongoDB instance using MongoDB Compass or the mongo shell
- Create a database called "e-commerce"
- Create collections for users, products, and orders
- Insert sample documents into each collection
- Practice CRUD operations using the MongoDB shell
Activity 2: Building a RESTful API with MongoDB and Express
Create a complete REST API for a blog application with:
- User authentication (signup, login)
- Blog posts with CRUD operations
- Comments as embedded documents within posts
- Categories for posts
- Ability to like posts
- Search functionality for posts
Implement proper error handling, validation, and pagination.
Activity 3: Data Modeling Challenge
Design a MongoDB data model for an online learning platform with:
- Users (students and instructors)
- Courses (with multiple lessons and quizzes)
- Enrollments
- Progress tracking for students
- Reviews and ratings for courses
Consider when to use embedded documents vs. references for optimal performance and flexibility.
MongoDB Best Practices
Schema Design Best Practices
- Design for Your Application's Access Patterns: Structure data based on how it will be queried
- Embed When Possible, Reference When Necessary: Choose the right data model based on relationships
- Avoid Unbounded Arrays: Be careful with arrays that might grow indefinitely
- Plan for Document Growth: Consider how documents might evolve over time
- Keep Document Size Reasonable: Avoid documents exceeding 16MB limit
- Use Schema Validation: Enforce data structure when needed
Performance Best Practices
- Create Proper Indexes: Index fields that are frequently queried
- Use Covered Queries: Design queries that can be satisfied entirely by an index
- Avoid Using $where Queries: These are slow and can't use indexes effectively
- Use Projection: Only retrieve the fields you need
- Limit Results: Use pagination for large result sets
- Use Aggregation Pipeline: Process data within the database when possible
Security Best Practices
- Enable Authentication: Require users to authenticate
- Implement Role-Based Access Control: Assign appropriate privileges
- Encrypt Sensitive Data: Use encryption for sensitive information
- Validate User Input: Prevent injection attacks by validating all input
- Enable TLS/SSL: Secure database connections
- Secure Network Access: Use firewalls and network security
Mongoose Best Practices
- Define Schemas with Validation: Leverage Mongoose's validation capabilities
- Use Middleware: Implement pre/post hooks for common operations
- Handle Errors Properly: Catch and handle Mongoose errors appropriately
- Use Lean Queries: For read-only operations, use .lean() to improve performance
- Implement Pagination: Use .skip() and .limit() for large collections
- Create Reusable Query Functions: Encapsulate common query patterns
MongoDB vs. PostgreSQL: When to Use Each
| Factor | MongoDB | PostgreSQL |
|---|---|---|
| Data Structure | Schema-flexible, document-oriented | Schema-rigid, table-oriented |
| Complex Relationships | Less natural, requires denormalization or references | Very natural with joins and foreign keys |
| Querying Flexibility | Rich document-oriented queries | Powerful SQL with advanced functions |
| Transactions | Supported since v4.0, with limitations | Fully ACID compliant with robust transaction support |
| Schema Evolution | Highly flexible, easy schema changes | Less flexible, requires migrations |
| Performance for Read-Heavy Workloads | Excellent for denormalized data | Good with proper indexing |
| Scalability | Designed for horizontal scaling | Traditionally vertical, with horizontal options |
| Data Consistency | Eventually consistent by default | Strongly consistent |
| Write Performance | Generally faster | Can be slower due to ACID guarantees |
Choose MongoDB When:
- Your data structure is subject to frequent changes
- You have document-oriented data with nested structures
- You need high write throughput
- You need horizontal scaling across many servers
- Your application requires rapid prototyping and iteration
- You have large volumes of unstructured or semi-structured data
- You're building applications like content management systems, IoT applications, or real-time analytics
Choose PostgreSQL When:
- Your data has complex relationships that benefit from joins
- You need strict data integrity and ACID compliance
- Your application requires complex transactions
- You need advanced SQL features (window functions, CTEs, etc.)
- You have a stable, well-defined schema
- You're working with financial data or other applications requiring strong consistency
- You're building applications like financial systems, inventory management, or traditional ERP systems
Hybrid Approaches
Many modern applications use both types of databases in a polyglot persistence architecture:
- PostgreSQL for transactional data, financial records, and complex relationships
- MongoDB for user-generated content, activity streams, logs, and flexible data
Further Reading and Resources
- MongoDB Official Documentation
- Mongoose Documentation
- MongoDB University (free online courses)
- "MongoDB: The Definitive Guide" by Shannon Bradshaw, Eoin Brazil, and Kristina Chodorow
- "MongoDB Applied Design Patterns" by Rick Copeland
- "Data Modeling for MongoDB" by Steve Hoberman