Skip to main content

MongoDB with Nodejs Driver Integration

Now that you've mastered MongoDB fundamentals and administration, it's time to bridge the gap between your Node.js applications and MongoDB databases. In this lesson, you'll learn how to integrate MongoDB with Node.js using the official MongoDB driver, enabling you to build full-stack applications with robust data persistence.

By the end of this lesson, you'll be able to:

  • Install and configure the MongoDB Node.js driver
  • Establish database connections with proper error handling
  • Perform CRUD operations programmatically
  • Implement connection pooling and best practices
  • Handle asynchronous operations effectively

Installing the MongoDB Driver

First, install the official MongoDB driver in your Node.js project:

Terminal
npm install mongodb

The driver provides a comprehensive API for interacting with MongoDB databases, collections, and documents from your Node.js applications.

Establishing Database Connections

Let's start by creating a connection to your MongoDB database. You can connect to a local instance or a MongoDB Atlas cluster.

db/connection.js
import { MongoClient } from 'mongodb';

// Connection URI - replace with your actual connection string
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017';

// Create a new MongoClient
const client = new MongoClient(uri);

// Database connection with error handling
async function connectToDatabase() {
try {
await client.connect();
console.log('Connected successfully to MongoDB server');
return client.db('mydatabase'); // Replace with your database name
} catch (error) {
console.error('Failed to connect to MongoDB:', error);
process.exit(1);
}
}

export { connectToDatabase, client };
tip

Always use environment variables for sensitive connection information like database URIs, especially when connecting to production databases or MongoDB Atlas clusters.

Connection Pooling and Best Practices

The MongoDB driver automatically manages connection pooling. Here's how to create a reusable database module:

db/database.js
import { MongoClient } from 'mongodb';

class Database {
constructor() {
this.client = null;
this.db = null;
}

async connect(uri, dbName) {
this.client = new MongoClient(uri, {
maxPoolSize: 10, // Maximum number of connections in the pool
minPoolSize: 5, // Minimum number of connections to maintain
socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity
});

await this.client.connect();
this.db = this.client.db(dbName);
console.log(`Connected to database: ${dbName}`);
}

async disconnect() {
if (this.client) {
await this.client.close();
console.log('Disconnected from MongoDB');
}
}

getCollection(collectionName) {
if (!this.db) {
throw new Error('Database not connected');
}
return this.db.collection(collectionName);
}
}

// Create a singleton instance
const database = new Database();
export default database;

Performing CRUD Operations

Now let's implement a service that performs CRUD operations on a users collection:

services/userService.js
import database from '../db/database.js';

class UserService {
constructor() {
this.collection = null;
}

async init() {
this.collection = database.getCollection('users');
}

// Create a new user
async createUser(userData) {
const result = await this.collection.insertOne(userData);
return result.insertedId;
}

// Find user by email
async findUserByEmail(email) {
return await this.collection.findOne({ email });
}

// Find multiple users with pagination
async findUsers(page = 1, limit = 10) {
const skip = (page - 1) * limit;
return await this.collection.find({})
.skip(skip)
.limit(limit)
.toArray();
}

// Update user information
async updateUser(email, updates) {
const result = await this.collection.updateOne(
{ email },
{ $set: updates }
);
return result.modifiedCount;
}

// Delete a user
async deleteUser(email) {
const result = await this.collection.deleteOne({ email });
return result.deletedCount;
}
}

export default UserService;

Complete Application Example

Here's how to use these components in a complete application:

app.js
import database from './db/database.js';
import UserService from './services/userService.js';

async function main() {
try {
// Connect to database
await database.connect(
process.env.MONGODB_URI || 'mongodb://localhost:27017',
'myapp'
);

// Initialize user service
const userService = new UserService();
await userService.init();

// Create a new user
const userId = await userService.createUser({
name: 'John Doe',
email: 'john@example.com',
age: 30,
createdAt: new Date()
});
console.log(`Created user with ID: ${userId}`);

// Find the user
const user = await userService.findUserByEmail('john@example.com');
console.log('Found user:', user);

// Update user age
const updated = await userService.updateUser('john@example.com', { age: 31 });
console.log(`Updated ${updated} user(s)`);

// List users with pagination
const users = await userService.findUsers(1, 5);
console.log('First 5 users:', users);

} catch (error) {
console.error('Application error:', error);
} finally {
// Close database connection
await database.disconnect();
}
}

main();

Error Handling and Async/Await

Proper error handling is crucial when working with databases:

utils/errorHandling.js
export class DatabaseError extends Error {
constructor(message, operation) {
super(message);
this.name = 'DatabaseError';
this.operation = operation;
}
}

export async function withRetry(operation, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries) {
throw new DatabaseError(
`Operation failed after ${maxRetries} attempts: ${error.message}`,
operation.name
);
}
console.warn(`Attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
warning

Always handle connection errors and implement retry logic for production applications. Network issues and temporary database unavailability are common in distributed systems.

Common Pitfalls

  • Not closing connections: Always close database connections when your application shuts down to prevent resource leaks
  • Ignoring connection errors: Implement proper error handling for connection failures and retry logic
  • Blocking the event loop: Avoid synchronous database operations that can block Node.js event loop
  • Hardcoding connection strings: Use environment variables for different deployment environments
  • Not using connection pooling: Let the driver manage connections efficiently instead of creating new connections for each operation
  • Forgetting to await operations: MongoDB driver methods return promises - always use await or handle promises properly
  • Ignoring index usage: Ensure your queries use appropriate indexes for better performance

Summary

You've learned how to integrate MongoDB with Node.js applications using the official driver. Key takeaways include establishing database connections with proper error handling, implementing connection pooling, performing CRUD operations programmatically, and following best practices for production applications. The MongoDB Node.js driver provides a robust, asynchronous API that integrates seamlessly with modern Node.js applications.

Show quiz
  1. What is the primary purpose of connection pooling in the MongoDB Node.js driver?

    • A) To encrypt database communications
    • B) To reuse database connections and improve performance
    • C) To automatically backup your data
    • D) To validate database schemas
  2. Which method should you call to properly close database connections when your application exits?

    • A) client.stop()
    • B) client.disconnect()
    • C) client.close()
    • D) client.terminate()
  3. What is the advantage of using environment variables for MongoDB connection strings?

    • A) They make your code run faster
    • B) They allow different configurations for development and production
    • C) They automatically create database indexes
    • D) They enable real-time data synchronization
  4. Why is it important to use async/await with MongoDB driver methods?

    • A) To make the code look more modern
    • B) Because all driver methods are synchronous
    • C) To properly handle asynchronous operations and avoid blocking the event loop
    • D) To automatically retry failed operations
  5. What should you do if a database connection fails in a production application?

    • A) Immediately crash the application
    • B) Implement retry logic with exponential backoff
    • C) Switch to a different database system
    • D) Log the error and continue without database functionality

Answers:

  1. B) To reuse database connections and improve performance
  2. C) client.close()
  3. B) They allow different configurations for development and production
  4. C) To properly handle asynchronous operations and avoid blocking the event loop
  5. B) Implement retry logic with exponential backoff