Back to all posts

MongoDB without Mongoose


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:

  1. Ensure appropriate indexes are in place
  2. Use .explain() to analyze query performance
  3. Use projection to limit returned fields
  4. 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.