MongoDB is a popular NoSQL database that stores data in flexible, JSON-like documents. While many developers use Mongoose as an ODM (Object Document Manper) for MongoDB. We can use MongoDB without Mongoose. Let’s explore
Installing the MongoDB Driver
Install the official MongoDB driver for Node.js:
npm install mongodb
Connecting the MongoDB
Basic Connection
const { MongoClient } = require('mongodb');
// Connection URL
const url = 'mongodb://localhost:27017';
// Database Name
const dbName = 'myProject';
// Create a new MongoClient
const client = new MongoClient(url);
async function connectToDatabase() {
try {
// Connect to the MongoDB server
await client.connect();
console.log('Connected successfully to MongoDB server');
// Get the database
const db = client.db(dbName);
return db;
} catch (error) {
console.error('Error connecting to MongoDB:', error);
throw error;
}
}
Connection with Options
const { MongoClient } = require('mongodb');
const uri = 'mongodb://localhost:27017';
const client = new MongoClient(uri, {
maxPoolSize: 50,
connectTimeoutMS: 5000,
useNewUrlParser: true,
useUnifiedTopology: true,
});
async function connectWithOptions() {
try {
await client.connect();
return client.db('myProject');
} catch (error) {
console.error('Connection error:', error);
throw error;
}
}
Connection String with Authentication
const uri = 'mongodb://username:password@localhost:27017/myProject';
const client = new MongoClient(uri);
MongoDB Atlas Connection
const uri = 'mongodb+srv://username:password@cluster0.mongodb.net/myProject';
const client = new MongoClient(uri);
Working with Collections
Collections in MongoDB are equivalent to tables in relational databases. They store documents (records).
async function getCollection() {
const db = await connectToDatabase();
const collection = db.collection('users');
return collection;
}
CRUD Operations
Create (Insert) Operations
Insert a Single Document
async function insertUser(userData) {
const collection = await getCollection();
try {
const result = await collection.insertOne(userData);
console.log(`Inserted document with _id: ${result.insertedId}`);
return result;
} catch (error) {
console.error('Error inserting document:', error);
throw error;
}
}
// Example usage
await insertUser({
name: 'John Doe',
email: 'john@example.com',
age: 30,
createdAt: new Date()
});
Insert Multiple Docuements
async function insertManyUsers(usersArray) {
const collection = await getCollection();
try {
const result = await collection.insertMany(usersArray);
console.log(`${result.insertedCount} documents were inserted`);
return result;
} catch (error) {
console.error('Error inserting documents:', error);
throw error;
}
}
// Example usage
await insertManyUsers([
{ name: 'John', email: 'john@example.com' },
{ name: 'Jane', email: 'jane@example.com' }
]);
Read Operations
Find a Single Document
async function findUserByEmail(email) {
const collection = await getCollection();
try {
const user = await collection.findOne({ email });
return user;
} catch (error) {
console.error('Error finding document:', error);
throw error;
}
}
Find Multiple Documents
async function findUsersByAge(minAge) {
const collection = await getCollection();
try {
const cursor = collection.find({ age: { $gte: minAge } });
const users = await cursor.toArray();
return users;
} catch (error) {
console.error('Error finding documents:', error);
throw error;
}
}
Find with Projection (Select Specific Fields)
async function findUsersByAge(minAge) {
const collection = await getCollection();
try {
const cursor = collection.find({ age: { $gte: minAge } });
const users = await cursor.toArray();
return users;
} catch (error) {
console.error('Error finding documents:', error);
throw error;
}
}
Pagination
async function getUsersWithPagination(page = 1, limit = 10) {
const collection = await getCollection();
try {
const skip = (page - 1) * limit;
const users = await collection.find()
.skip(skip)
.limit(limit)
.toArray();
const total = await collection.countDocuments();
return {
users,
totalPages: Math.ceil(total / limit),
currentPage: page
};
} catch (error) {
console.error('Error with pagination:', error);
throw error;
}
}
Update Operations
Update a Single Document
async function updateUserEmail(userId, newEmail) {
const collection = await getCollection();
try {
const result = await collection.updateOne(
{ _id: new ObjectId(userId) },
{ $set: { email: newEmail, updatedAt: new Date() } }
);
console.log(`Modified ${result.modifiedCount} document(s)`);
return result;
} catch (error) {
console.error('Error updating document:', error);
throw error;
}
}
Update Multiple Documents
async function updateUserStatus(status) {
const collection = await getCollection();
try {
const result = await collection.updateMany(
{ active: true },
{ $set: { status, updatedAt: new Date() } }
);
console.log(`Modified ${result.modifiedCount} document(s)`);
return result;
} catch (error) {
console.error('Error updating documents:', error);
throw error;
}
}
FindOneAndUpdate
async function findAndUpdateUser(userId, update) {
const collection = await getCollection();
try {
// Returns the updated document by default
const result = await collection.findOneAndUpdate(
{ _id: new ObjectId(userId) },
{ $set: { ...update, updatedAt: new Date() } },
{ returnDocument: 'after' } // Use 'before' to get the original document
);
return result.value;
} catch (error) {
console.error('Error finding and updating document:', error);
throw error;
}
}
Delete Operations
Delete a Single Document
async function deleteUser(userId) {
const collection = await getCollection();
try {
const result = await collection.deleteOne({ _id: new ObjectId(userId) });
console.log(`Deleted ${result.deletedCount} document(s)`);
return result;
} catch (error) {
console.error('Error deleting document:', error);
throw error;
}
}
Delete Multiple Documentions
async function deleteInactiveUsers() {
const collection = await getCollection();
try {
const result = await collection.deleteMany({ active: false });
console.log(`Deleted ${result.deletedCount} document(s)`);
return result;
} catch (error) {
console.error('Error deleting documents:', error);
throw error;
}
}
Indexing
Indexing improve query performance and can uniqueness contstraints.
Creating an Index
async function createEmailIndex() {
const collection = await getCollection();
try {
const result = await collection.createIndex(
{ email: 1 },
{ unique: true }
);
console.log(`Index created: ${result}`);
return result;
} catch (error) {
console.error('Error creating index:', error);
throw error;
}
}
Creating a Compound Index
async function createCompoundIndex() {
const collection = await getCollection();
try {
const result = await collection.createIndex(
{ lastName: 1, firstName: 1 }
);
console.log(`Index created: ${result}`);
return result;
} catch (error) {
console.error('Error creating compound index:', error);
throw error;
}
}
List All Indexes
async function listIndexes() {
const collection = await getCollection();
try {
const indexes = await collection.indexes();
return indexes;
} catch (error) {
console.error('Error listing indexes:', error);
throw error;
}
}
Drop an Index
async function dropIndex(indexName) {
const collection = await getCollection();
try {
await collection.dropIndex(indexName);
console.log(`Index ${indexName} dropped`);
} catch (error) {
console.error('Error dropping index:', error);
throw error;
}
}
Aggregation Framework
The Aggregation Framework is MongoDB’s powerful data processing tool similar to SQL’s `GROUP BY` and JOIN operations.
Basic Aggregation
async function getUserAgeStats() { const collection = await getCollection(); try { const result = await collection.aggregate([ { $group: { _id: null, avgAge: { $avg: "$age" }, minAge: { $min: "$age" }, maxAge: { $max: "$age" }, totalUsers: { $sum: 1 } } } ]).toArray(); return result[0]; } catch (error) { console.error('Error with aggregation:', error); throw error; } }
Grouping by a Field
async function getUsersByCountry() {
const collection = await getCollection();
try {
const result = await collection.aggregate([
{
$group: {
_id: "$country",
count: { $sum: 1 },
users: { $push: "$name" }
}
},
{
$sort: { count: -1 }
}
]).toArray();
return result;
} catch (error) {
console.error('Error with aggregation:', error);
throw error;
}
}
Multi-Stage Aggregation Pipeline
async function getActiveUserStats() {
const collection = await getCollection();
try {
const result = await collection.aggregate([
// Filter documents
{
$match: {
active: true,
age: { $gte: 18 }
}
},
// Group by country
{
$group: {
_id: "$country",
userCount: { $sum: 1 },
averageAge: { $avg: "$age" }
}
},
// Sort by user count
{
$sort: { userCount: -1 }
},
// Limit to top 5
{
$limit: 5
}
]).toArray();
return result;
} catch (error) {
console.error('Error with aggregation pipeline:', error);
throw error;
}
}
Lookup (Similar to JOIN)
async function getUsersWithOrders() {
const db = await connectToDatabase();
const usersCollection = db.collection('users');
try {
const result = await usersCollection.aggregate([
{
$lookup: {
from: "orders",
localField: "_id",
foreignField: "userId",
as: "orders"
}
}
]).toArray();
return result;
} catch (error) {
console.error('Error with lookup aggregation:', error);
throw error;
}
}
Transactions
MongoDB supports multi-document transactions starting from version 4.0 for replica sets and 4.2 for sharded clusters.
async function transferFunds(fromAccountId, toAccountId, amount) {
const client = new MongoClient(url);
try {
await client.connect();
const db = client.db('finance');
const accounts = db.collection('accounts');
// Start a transaction
const session = client.startSession();
try {
session.startTransaction();
// Deduct from sender
await accounts.updateOne(
{ _id: new ObjectId(fromAccountId) },
{ $inc: { balance: -amount } },
{ session }
);
// Add to receiver
await accounts.updateOne(
{ _id: new ObjectId(toAccountId) },
{ $inc: { balance: amount } },
{ session }
);
// Commit the transaction
await session.commitTransaction();
console.log('Transaction committed successfully');
} catch (error) {
// Abort transaction on error
await session.abortTransaction();
console.error('Transaction aborted:', error);
throw error;
} finally {
await session.endSession();
}
} finally {
await client.close();
}
}
Error Handling and Best Practices
Error Handling
async function safeMongoOperation(operation) {
try {
return await operation();
} catch (error) {
// Handle different types of MongoDB errors
if (error.name === 'MongoServerError') {
if (error.code === 11000) {
console.error('Duplicate key error:', error.message);
throw new Error('A record with this value already exists');
}
}
console.error('MongoDB operation failed:', error);
throw error;
}
}
// Example usage
async function safeInsertUser(userData) {
return safeMongoOperation(async () => {
const collection = await getCollection();
return collection.insertOne(userData);
});
}
Connection Pooling
MongoDB’s driver automatically manages connection pooling. Configure it properly for your application:
const client = new MongoClient(uri, {
maxPoolSize: 100, // Maximum number of connections in the pool
minPoolSize: 5, // Minimum number of connections in the pool
maxIdleTimeMS: 30000 // Close connections after 30 seconds of inactivity
});
Closing Connections
Properly close connections when your applications shuts down:
async function gracefulShutdown() {
console.log('Closing MongoDB connection...');
try {
await client.close();
console.log('MongoDB connection closed');
} catch (error) {
console.error('Error closing MongoDB connection:', error);
process.exit(1);
}
}
// Handle application termination
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);
Data Modeling
Without Mongoose, you’ll need to implement your own data modeling and validation.
Simple Model Example
// User model implementation
class User {
constructor(data) {
this.data = {
_id: data._id || null,
name: data.name || '',
email: data.email || '',
age: data.age || null,
createdAt: data.createdAt || new Date(),
updatedAt: data.updatedAt || new Date()
};
}
// Simple validation
validate() {
if (!this.data.name) throw new Error('Name is required');
if (!this.data.email) throw new Error('Email is required');
if (!/^\S+@\S+\.\S+$/.test(this.data.email)) {
throw new Error('Invalid email format');
}
return true;
}
// Save to database
async save() {
this.validate();
const collection = await getCollection();
if (this.data._id) {
// Update existing user
this.data.updatedAt = new Date();
const result = await collection.updateOne(
{ _id: this.data._id },
{ $set: this.data }
);
return result;
} else {
// Create new user
const result = await collection.insertOne(this.data);
this.data._id = result.insertedId;
return result;
}
}
// Find by ID
static async findById(id) {
const collection = await getCollection();
const data = await collection.findOne({ _id: new ObjectId(id) });
return data ? new User(data) : null;
}
// Find by criteria
static async find(criteria) {
const collection = await getCollection();
const results = await collection.find(criteria).toArray();
return results.map(data => new User(data));
}
}
Performance Optimization
Using Projection
Only return the fields you need:
const users = await collection.find(
{ country: 'USA' },
{ projection: { name: 1, email: 1, _id: 0 } }
).toArray();
Using Explain for Query Analysis
async function analyzeQuery() {
const collection = await getCollection();
const explanation = await collection.find({ age: { $gt: 25 } })
.explain('executionStats');
console.log(JSON.stringify(explanation, null, 2));
return explanation;
}
Limiting Returned Results
// Limit to 100 documents
const users = await collection.find()
.limit(100)
.toArray();
Using Appropriate Indexes
// Create an index for frequently queried fields
await collection.createIndex({ email: 1 });
// Compound index for queries that filter on multiple fields
await collection.createIndex({ age: 1, country: 1 });
Security Considerations
Input Validation
Always validate and sanitize user input before creating queries:
function sanitizeInput(input) {
// Simple example - implement according to your needs
if (typeof input !== 'string') return input;
// Prevent NoSQL injection by removing $ operators and .
return input.replace(/[$\.]/g, '');
}
async function safeFind(userInput) {
const safeInput = sanitizeInput(userInput);
const collection = await getCollection();
return collection.find({ name: safeInput }).toArray();
}
Authentication
Use MongoDB’s authentication mechanisms:
const uri = 'mongodb://username:password@localhost:27017/myProject?authSource=admin';
const client = new MongoClient(uri);
Depoyment Strategies
Replica Sets
For high available, use MongoDB replica sets:
const uri = 'mongodb://server1:27017,server2:27017,server3:27017/myProject?replicaSet=rs0';
const client = new MongoClient(uri);
Connection to Sharded Cluster
const uri = 'mongodb://mongos1:27017,mongos2:27017/myProject';
const client = new MongoClient(uri);
Monitoring and Logging
Basic Logging
const { MongoClient } = require('mongodb');
const logger = require('./your-logger'); // Your logging solution
// With logging
async function executeWithLogging(operation, description) {
const startTime = Date.now();
try {
logger.info(`Starting operation: ${description}`);
const result = await operation();
const duration = Date.now() - startTime;
logger.info(`Completed operation: ${description} in ${duration}ms`);
return result;
} catch (error) {
logger.error(`Error in operation: ${description}`, error);
throw error;
}
}
// Example usage
async function findUsersWithLogging(query) {
return executeWithLogging(
async () => {
const collection = await getCollection();
return collection.find(query).toArray();
},
`Finding users with query: ${JSON.stringify(query)}`
);
}
Troubleshooting Common Issues
Connection Issues
async function testConnection() {
const client = new MongoClient(uri, {
connectTimeoutMS: 5000,
serverSelectionTimeoutMS: 5000
});
try {
await client.connect();
await client.db('admin').command({ ping: 1 });
console.log('Connection successful!');
return true;
} catch (error) {
console.error('Connection failed:', error);
return false;
} finally {
await client.close();
}
}
Query Performance Issues
If queries are slow:
- Ensure appropriate indexes are in place
- Use
.explain()to analyze query performance - Use projection to limit returned fields
- Consider updating your schema design
// Check if an index exists
async function checkForIndex(indexName) {
const collection = await getCollection();
const indexes = await collection.indexes();
const indexExists = indexes.some(index => index.name === indexName);
if (!indexExists) {
console.warn(`Index ${indexName} does not exist! This may cause performance issues.`);
}
return indexExists;
}
Working directly with the MongoDB driver gives you full control over your database operations and can lead to better performance without the overhead of an ODM like Mongoose.