Multiple MongoDB Connections in a Single Application
Rate this tutorial
MongoDB, a popular NoSQL database, is widely used in various applications and scenarios. While a single database connection can adequately serve the needs of numerous projects, there are specific scenarios and various real-world use cases that highlight the advantages of employing multiple connections.
In this article, we will explore the concept of establishing multiple MongoDB connections within a single Node.js application.
In the world of MongoDB and data-driven applications, the demand for multiple MongoDB connections is on the rise. Let's explore why this need arises and discover real-world use cases and examples where multiple connections provide a vital solution.
Sectors such as e-commerce, gaming, financial services, media, entertainment, and the Internet of Things (IoT) frequently contend with substantial data volumes or data from diverse sources.
For instance, imagine a web application that distributes traffic evenly across several MongoDB servers using multiple connections or a microservices architecture where each microservice accesses the database through its dedicated connection. Perhaps in a data processing application, multiple connections allow data retrieval from several MongoDB servers simultaneously. Even a backup application can employ multiple connections to efficiently back up data from multiple MongoDB servers to a single backup server.
Moreover, consider a multi-tenant application where different tenants or customers share the same web application but require separate, isolated databases. In this scenario, each tenant can have their own dedicated MongoDB connection. This ensures data separation, security, and customization for each tenant while all operating within the same application. This approach simplifies management and provides an efficient way to scale as new tenants join the platform without affecting existing ones.
Before we delve into practical implementation, let's introduce some key concepts that will be relevant in the upcoming use case examples. Consider various use cases such as load balancing, sharding, read replicas, isolation, and fault tolerance. These concepts play a crucial role in scenarios where multiple MongoDB connections are required for efficient data management and performance optimization.
Throughout this guide, we'll be using Node.js, Express.js, and the Mongoose NPM package for managing MongoDB interactions. Before proceeding, ensure that your development environment is ready and that you have these dependencies installed.
If you are new to MongoDB or haven't set up MongoDB before, the first step is to set up a MongoDB Atlas account. You can find step-by-step instructions on how to do this in the MongoDB Getting Started with Atlas article.
This post uses MongoDB 6.3.2 and Node.js 18.17.1
If you're planning to create a new project, start by creating a fresh directory for your project. Then, initiate a new project using the
npm init
command.If you already have an existing project and want to integrate these dependencies, ensure you have the project's directory open. In this case, you only need to install the dependencies Express and Mongoose if you haven’t already, making sure to specify the version numbers to prevent any potential conflicts.
1 npm i express@4.18.2 mongoose@7.5.3
Please be aware that Mongoose is not the official MongoDB driver but a
popular Object Data Modelling (ODM) library for MongoDB. If you prefer
to use the official MongoDB driver, you can find relevant
documentation on the MongoDB official
website.
The next step is to set up the environment
.env
file if you haven't already. We will define variables for the MongoDB connection strings that we will use throughout this article. The PRIMARY_CONN_STR
variable is for the primary MongoDB connection string, and the SECONDARY_CONN_STR
variable is for the secondary MongoDB connection string.1 PRIMARY_CONN_STR=mongodb+srv://… 2 SECONDARY_CONN_STR=mongodb+srv://…
If you are new to MongoDB and need guidance on obtaining a MongoDB connection string from Atlas, please refer to the Get Connection String article.
Now, we'll break down the connection process into two parts: one for the primary connection and the other for the secondary connection.
Now, let's begin by configuring the primary connection.
The primary connection process might be familiar to you if you've already implemented it in your application. However, I'll provide a detailed explanation for clarity. Readers who are already familiar with this process can skip this section.
We commonly utilize the mongoose.connect() method to establish the primary MongoDB database connection for our application, as it efficiently manages a single connection pool for the entire application.
In a separate file named
db.primary.js
, we define a connection method that we'll use in our main application file (for example, index.js
). This method, shown below, configures the MongoDB connection and handles events:1 const mongoose = require("mongoose"); 2 3 module.exports = (uri, options = {}) => { 4 // By default, Mongoose skips properties not defined in the schema (strictQuery). Adjust it based on your configuration. 5 mongoose.set('strictQuery', true); 6 7 // Connect to MongoDB 8 mongoose.connect(uri, options) 9 .then() 10 .catch(err => console.error("MongoDB primary connection failed, " + err)); 11 12 // Event handling 13 mongoose.connection.once('open', () => console.info("MongoDB primary connection opened!")); 14 mongoose.connection.on('connected', () => console.info("MongoDB primary connection succeeded!")); 15 mongoose.connection.on('error', (err) => { 16 console.error("MongoDB primary connection failed, " + err); 17 mongoose.disconnect(); 18 }); 19 mongoose.connection.on('disconnected', () => console.info("MongoDB primary connection disconnected!")); 20 21 // Graceful exit 22 process.on('SIGINT', () => { 23 mongoose.connection.close().then(() => { 24 console.info("Mongoose primary connection disconnected through app termination!"); 25 process.exit(0); 26 }); 27 }); 28 }
The next step is to create schemas for performing operations in your application. We will write the schema in a separate file named
product.schema.js
and export it. Let's take an example schema for products in a stores application:1 const mongoose = require("mongoose"); 2 3 module.exports = (options = {}) => { 4 // Schema for Product 5 return new mongoose.Schema( 6 { 7 store: { 8 _id: mongoose.Types.ObjectId, // Reference-id to the store collection 9 name: String 10 }, 11 name: String 12 // add required properties 13 }, 14 options 15 ); 16 }
Now, let’s import the
db.primary.js
file in our main file (for example, index.js
) and use the method defined there to establish the primary MongoDB connection. You can also pass an optional connection options object if needed.After setting up the primary MongoDB connection, you import the
product.schema.js
file to access the Product Schema. This enables you to create a model and perform operations related to products in your application:1 // Primary Connection (Change the variable name as per your .env configuration!) 2 // Establish the primary MongoDB connection using the connection string variable declared in the Prerequisites section. 3 require("./db.primary.js")(process.env.PRIMARY_CONN_STR, { 4 // (optional) connection options 5 }); 6 7 // Import Product Schema 8 const productSchema = require("./product.schema.js")({ 9 collection: "products", 10 // Pass configuration options if needed 11 }); 12 13 // Create Model 14 const ProductModel = mongoose.model("Product", productSchema); 15 16 // Execute Your Operations Using ProductModel Object 17 (async function () { 18 let product = await ProductModel.findOne(); 19 console.log(product); 20 })();
Now, let's move on to setting up a secondary or second MongoDB connection for scenarios where your application requires multiple MongoDB connections.
Depending on your application's requirements, you can configure secondary MongoDB connections for various use cases. But before that, we'll create a connection code in a
db.secondary.js
file, specifically utilizing the mongoose.createConnection() method. This method allows us to establish separate connection pools each tailored to a specific use case or data access pattern, unlike the mongoose.connect()
method that we used previously for the primary MongoDB connection:1 const mongoose = require("mongoose"); 2 3 module.exports = (uri, options = {}) => { 4 // Connect to MongoDB 5 const db = mongoose.createConnection(uri, options); 6 7 // By default, Mongoose skips properties not defined in the schema (strictQuery). Adjust it based on your configuration. 8 db.set('strictQuery', true); 9 10 // Event handling 11 db.once('open', () => console.info("MongoDB secondary connection opened!")); 12 db.on('connected', () => console.info(`MongoDB secondary connection succeeded!`)); 13 db.on('error', (err) => { 14 console.error(`MongoDB secondary connection failed, ` + err); 15 db.close(); 16 }); 17 db.on('disconnected', () => console.info(`MongoDB secondary connection disconnected!`)); 18 19 // Graceful exit 20 process.on('SIGINT', () => { 21 db.close().then(() => { 22 console.info(`Mongoose secondary connection disconnected through app termination!`); 23 process.exit(0); 24 }); 25 }); 26 27 // Export db object 28 return db; 29 }
Now, let’s import the
db.secondary.js
file in our main file (for example, index.js
), create the connection object with a variable named db
, and use the method defined there to establish the secondary MongoDB connection. You can also pass an optional connection options object if needed:1 // Secondary Connection (Change the variable name as per your .env configuration!) 2 // Establish the secondary MongoDB connection using the connection string variable declared in the Prerequisites section. 3 const db = require("./db.secondary.js")(process.env.SECONDARY_CONN_STR, { 4 // (optional) connection options 5 });
Now that we are all ready with the connection, you can use that
db
object to create a model. We explore different scenarios and examples to help you choose the setup that best aligns with your specific data access and management needs:You can choose to use the same schema
product.schema.js
file that was employed in the primary connection. This is suitable for scenarios where both connections will operate on the same data model.Import the
product.schema.js
file to access the Product Schema. This enables you to create a model using db
object and perform operations related to products in your application:1 // Import Product Schema 2 const secondaryProductSchema = require("./product.schema.js")({ 3 collection: "products", 4 // Pass configuration options if needed 5 }); 6 7 // Create Model 8 const SecondaryProductModel = db.model("Product", secondaryProductSchema); 9 10 // Execute Your Operations Using SecondaryProductModel Object 11 (async function () { 12 let product = await SecondaryProductModel.findOne(); 13 console.log(product); 14 })();
To see a practical code example and available resources for using the existing schema of a primary database connection into a secondary MongoDB connection in your project, visit the GitHub repository.
When working with multiple MongoDB connections, it's essential to have the flexibility to adapt your schema based on specific use cases. While the primary connection may demand a strict schema with validation to ensure data integrity, there are scenarios where a secondary connection serves a different purpose. For instance, a secondary connection might store data for analytics on an archive server, with varying schema requirements driven by past use cases. In this section, we'll explore how to configure schema flexibility for your secondary connection, allowing you to meet the distinct needs of your application.
If you prefer to have schema flexibility in mongoose, you can pass the
strict: false
property in the options when configuring your schema for the secondary connection. This allows you to work with data that doesn't adhere strictly to the schema.Import the
product.schema.js
file to access the Product Schema. This enables you to create a model using db
object and perform operations related to products in your application:1 // Import Product Schema 2 const secondaryProductSchema = require("./product.schema.js")({ 3 collection: "products", 4 strict: false 5 // Pass configuration options if needed 6 }); 7 8 // Create Model 9 const SecondaryProductModel = db.model("Product", secondaryProductSchema); 10 11 // Execute Your Operations Using SecondaryProductModel Object 12 (async function () { 13 let product = await SecondaryProductModel.findOne(); 14 console.log(product); 15 })();
To see a practical code example and available resources for setting schema flexibility in a secondary MongoDB connection in your project, visit the GitHub repository.
Within your application's database setup, you can seamlessly switch between different databases using the db.useDb() method. This method enables you to create a new connection object associated with a specific database while sharing the same connection pool.
This approach allows you to efficiently manage multiple databases within your application, using a single connection while maintaining distinct data contexts for each database.
Import the
product.schema.js
file to access the Product Schema. This enables you to create a model using db
object and perform operations related to products in your application.Now, to provide an example where a store can have its own database containing users and products, you can include the following scenario.
Example use case: Store with separate database
Imagine you're developing an e-commerce platform where multiple stores operate independently. Each store has its database to manage its products. In this scenario, you can use the
db.useDb()
method to switch between different store databases while maintaining a shared connection pool:1 // Import Product Schema 2 const secondaryProductSchema = require("./product.schema.js")({ 3 collection: "products", 4 // strict: false // that doesn't adhere strictly to the schema! 5 // Pass configuration options if needed 6 }); 7 8 // Create a connection for 'Store A' 9 const storeA = db.useDb('StoreA'); 10 11 // Create Model 12 const SecondaryStoreAProductModel = storeA.model("Product", secondaryProductSchema); 13 14 // Execute Your Operations Using SecondaryStoreAProductModel Object 15 (async function () { 16 let product = await SecondaryStoreAProductModel.findOne(); 17 console.log(product); 18 })(); 19 20 // Create a connection for 'Store B' 21 const storeB = db.useDb('StoreB'); 22 23 // Create Model 24 const SecondaryStoreBProductModel = storeB.model("Product", secondaryProductSchema); 25 26 // Execute Your Operations Using SecondaryStoreBProductModel Object 27 (async function () { 28 let product = await SecondaryStoreBProductModel.findOne(); 29 console.log(product); 30 })();
In this example, separate database connections have been established for
Store A
and Store B
, each containing its product data. This approach provides a clear separation of data while efficiently utilizing a single shared connection pool for all stores, enhancing data management in a multi-store e-commerce platform.In the previous section, we demonstrated a static approach where connections were explicitly created for each store, and each connection was named accordingly (e.g.,
StoreA
, StoreB
).To introduce a dynamic approach, you can create a function that accepts a store's ID or name as a parameter and returns a connection object. This dynamic function allows you to switch between different stores by providing their identifiers, and it efficiently reuses existing connections when possible.
1 // Function to get connection object for particular store's database 2 function getStoreConnection(storeId) { 3 return db.useDb("Store"+storeId, { useCache: true }); 4 } 5 6 // Create a connection for 'Store A' 7 const store = getStoreConnection("A"); 8 9 // Create Model 10 const SecondaryStoreProductModel = store.model("Product", secondaryProductSchema); 11 12 // Execute Your Operations Using SecondaryStoreProductModel Object 13 (async function () { 14 let product = await SecondaryStoreProductModel.findOne(); 15 console.log(product); 16 })();
In the dynamic approach, connection instances are created and cached as needed, eliminating the need for manually managing separate connections for each store. This approach enhances flexibility and resource efficiency in scenarios where you need to work with multiple stores in your application.
By exploring these examples, we've covered a range of scenarios for managing multiple databases within the same connection, providing you with the flexibility to tailor your database setup to your specific application needs. You're now equipped to efficiently manage distinct data contexts for various use cases within your application.
To see a practical code example and available resources for switching databases within the same connection into a secondary MongoDB connection in your project, visit the GitHub repository.
In the pursuit of a robust and efficient MongoDB setup within your Node.js application, I recommend the following best practices. These guidelines serve as a foundation for a reliable implementation, and I encourage you to consider and implement them:
- Connection pooling: Make the most of connection pooling to efficiently manage MongoDB connections, enabling connection reuse and reducing overhead. Read more about connection pooling.
- Error handling: Robust error-handling mechanisms, comprehensive logging, and contingency plans ensure the reliability of your MongoDB setup in the face of unexpected issues.
- Security: Prioritize data security with authentication, authorization, and secure communication practices, especially when dealing with sensitive information. Read more about MongoDB Security.
- Scalability: Plan for scalability from the outset, considering both horizontal and vertical scaling strategies to accommodate your application's growth.
- Testing: Comprehensive testing in various scenarios, such as failover, high load, and resource constraints, validates the resilience and performance of your multiple MongoDB connection setup.
Leveraging multiple MongoDB connections in a Node.js application opens up a world of possibilities for diverse use cases, from e-commerce to multi-tenant systems. Whether you need to enhance data separation, scale your application efficiently, or accommodate different data access patterns, these techniques empower you to tailor your database setup to the unique needs of your project. With the knowledge gained in this guide, you're well-prepared to manage multiple data contexts within a single application, ensuring robust, flexible, and efficient MongoDB interactions.
- Mongoose documentation: For an in-depth understanding of Mongoose connections, explore the official Mongoose documentation.
- GitHub repository: To dive into the complete implementation of multiple MongoDB connections in a Node.js application that we have performed above, visit the GitHub repository. Feel free to clone the repository and experiment with different use cases in your projects.
If you have any questions or feedback, check out the MongoDB Community Forums and let us know what you think.