Connecting to MongoDB from Node.js
Before your Node.js application can read or write any data, it needs a live connection to a MongoDB server (or cluster). The official MongoDB Node.js driver handles this through a single object called MongoClient, which manages authentication, network sockets, and a pool of reusable connections behind a simple promise-based API. Getting this connection step right — and, just as importantly, not repeating it unnecessarily — is one of the most common sources of bugs and performance problems in real Node.js apps, so it’s worth understanding thoroughly before you write a single query.
Overview / How it works
MongoDB doesn’t speak HTTP. Clients talk to it over a custom binary wire protocol (BSON-encoded messages) sent over a plain TCP socket, usually wrapped in TLS. The Node.js driver’s job is to hide that protocol behind familiar JavaScript methods like insertOne and find. To do that, it needs three things from you: where the server is (a connection string), how to authenticate (credentials, usually inside that same string), and which database and collection to operate on once connected.
You install the driver with npm install mongodb, then create one MongoClient instance for your entire application — not one per request, not one per query. A single MongoClient internally manages a connection pool: a set of already-open, already-authenticated TCP sockets (by default up to 100) that are reused across every operation your app performs. When you call find() or insertOne(), the driver borrows a socket from the pool, sends the request, waits for the response, and returns the socket to the pool for the next operation. This is dramatically cheaper than opening a new TCP connection and re-authenticating for every single query, and it’s the single most important mental model to get right in this lesson.
The driver also continuously monitors your deployment’s topology — a standalone server, a replica set, or a sharded cluster — via periodic heartbeat pings, so it always knows which node is the current primary (for replica sets) and can route reads and writes correctly, even after a failover.
Syntax
import { MongoClient } from "mongodb";
const client = new MongoClient(connectionString, options);
await client.connect();
const db = client.db("databaseName");
// ... use db.collection("name") to run operations ...
await client.close();
- connectionString — a URI in the form
mongodb://user:password@host:port/dbName(standalone/replica set) ormongodb+srv://user:password@cluster-url/dbName(Atlas, uses DNS SRV lookup to discover all cluster members from a single hostname). - options — an object of connection settings; the most commonly used are listed below.
client.connect()— opens the initial sockets and runs the authentication handshake. Returns a promise; must be awaited before running operations (in driver v6, most operations will actually queue and wait automatically, but explicit connecting is still best practice and required to catch connection errors early).client.db(name)— returns a handle to a database on the connected cluster. Cheap and synchronous — it does not itself talk to the network.client.close()— gracefully closes every socket in the pool. Call this when your script/process is shutting down, not after every operation.
| Option | Purpose |
|---|---|
maxPoolSize |
Maximum sockets kept open per server (default 100). Raise for high-concurrency servers, lower for constrained environments. |
minPoolSize |
Sockets to keep warm even when idle, avoiding cold-start latency on the next burst of traffic. |
serverSelectionTimeoutMS |
How long to wait for a suitable server (e.g. a primary) before throwing an error (default 30000ms). |
connectTimeoutMS |
How long to wait while establishing a single TCP connection. |
retryWrites |
Automatically retries a write once if it fails due to a transient network error or failover. Defaults to true on modern connection strings. |
w (write concern) |
How many replica set members must acknowledge a write before it’s considered successful, e.g. "majority". |
Examples
Example 1: A minimal connection check. This is the pattern you’d use to sanity-check that your app can reach the database at all — connect, ping, close.
import { MongoClient } from "mongodb";
const uri = "mongodb://<user>:<password>@<cluster-url>/mydb";
const client = new MongoClient(uri);
async function main() {
try {
await client.connect();
await client.db("admin").command({ ping: 1 });
console.log("Connected successfully to MongoDB");
} finally {
await client.close();
}
}
main().catch(console.error);
Output:
Connected successfully to MongoDB
The { ping: 1 } admin command is the idiomatic way to verify a live connection without touching any real data. The try/finally guarantees client.close() runs even if the ping throws, which matters most in short-lived scripts (a long-running server would skip the close() and keep the pool open for the app’s lifetime instead).
Example 2: Connecting to Atlas and inserting a document. This uses the mongodb+srv:// form, which Atlas gives you on its connection dashboard.
import { MongoClient } from "mongodb";
const uri = "mongodb+srv://<user>:<password>@<cluster-url>/mydb?retryWrites=true&w=majority";
const client = new MongoClient(uri);
async function insertSampleUser() {
await client.connect();
const db = client.db("shop_app");
const users = db.collection("users");
const result = await users.insertOne({
name: "Priya Sharma",
email: "priya@example.com",
createdAt: new Date()
});
console.log(`Inserted user with _id: ${result.insertedId}`);
await client.close();
}
insertSampleUser().catch(console.error);
Output:
Inserted user with _id: 66b1f2a4c3d4e5f6a7b8c9d0
Note that <cluster-url> here is a single DNS name; behind the scenes the driver performs an SRV lookup against it to discover every node in the replica set, then opens pooled connections to each one it needs.
Example 3: The connection-pooling pattern for a server (e.g. Express). A web server should create the client once, at startup, and reuse it for the lifetime of the process.
import { MongoClient } from "mongodb";
const uri = "mongodb+srv://<user>:<password>@<cluster-url>/mydb";
const client = new MongoClient(uri, {
maxPoolSize: 20,
serverSelectionTimeoutMS: 5000
});
let dbConnection;
async function connectToDatabase() {
if (dbConnection) return dbConnection;
await client.connect();
dbConnection = client.db("shop_app");
console.log("MongoDB connection pool established");
return dbConnection;
}
// Elsewhere in the app, e.g. inside a request handler:
async function getOrderById(orderId) {
const db = await connectToDatabase();
return db.collection("orders").findOne({ _id: orderId });
}
Output:
MongoDB connection pool established
The dbConnection variable acts as a cache: the first request triggers the actual connect, and every subsequent request reuses the same pooled handle instantly, with no repeated authentication or socket setup.
How it works step by step
- Parse the URI. The driver reads the scheme (
mongodb://ormongodb+srv://), extracts host(s), credentials, the default database, and query-string options. - Discover topology. For
mongodb+srv://, it performs a DNS SRV lookup to resolve the actual list of cluster members; formongodb://, it uses the hosts given directly. It then contacts each node to learn its role (primary, secondary, mongos, etc.). - Open sockets and authenticate. A TCP connection (typically wrapped in TLS) is opened to the selected server(s), and a SCRAM authentication handshake exchanges the credentials from the URI — the plaintext password is never sent over the wire.
- Populate the pool. Up to
minPoolSizeconnections are opened eagerly; more are opened on demand up tomaxPoolSizeas concurrent operations require them. - Route and execute operations. Each call like
insertOne()orfind()borrows an available socket, serializes your JavaScript object into BSON, sends it, and deserializes the BSON response back into JavaScript objects/promises. - Monitor continuously. Background heartbeats keep checking server health and topology; if a replica set primary changes during a failover, the driver detects this and reroutes writes to the new primary, retrying once automatically if
retryWritesis enabled. - Shut down cleanly.
client.close()closes every pooled socket. Skipping this in a short script causes it to hang, because open sockets keep the Node.js event loop alive.
Common Mistakes
Mistake 1: creating a new MongoClient on every request. This defeats connection pooling entirely — every request pays the full connect-and-authenticate cost, and under load you can exhaust available sockets or hit the server’s connection limit.
// WRONG: opens a brand-new connection pool on every request
app.get("/users", async (req, res) => {
const client = new MongoClient(uri);
await client.connect();
const users = await client.db("shop_app").collection("users").find().toArray();
res.json(users);
await client.close();
});
// RIGHT: connect once at startup, reuse the same client/pool for every request
const client = new MongoClient(uri, { maxPoolSize: 20 });
await client.connect();
app.get("/users", async (req, res) => {
const users = await client.db("shop_app").collection("users").find().toArray();
res.json(users);
});
Mistake 2: comparing a route parameter directly to _id. req.params.id is always a string, but a document’s _id is usually a BSON ObjectId. A plain string never matches an ObjectId, so the query silently returns nothing instead of erroring.
// WRONG: req.params.id is a string, _id is stored as ObjectId — this never matches
const user = await db.collection("users").findOne({ _id: req.params.id });
// RIGHT: convert the string to an ObjectId first
import { ObjectId } from "mongodb";
const user = await db.collection("users").findOne({ _id: new ObjectId(req.params.id) });
Mistake 3: unescaped special characters in the password. If your password contains characters like @, :, or /, they must be percent-encoded, or the URI parser will misread the host portion of the connection string and connection will fail with a confusing error. Use encodeURIComponent(password) when building the URI dynamically, or escape the characters by hand.
Best Practices
- Create exactly one
MongoClientper application process and reuse it; let the driver’s pool handle concurrency instead of managing connections yourself. - Never hardcode credentials in source code — load the connection string from an environment variable (e.g.
process.env.MONGODB_URI). - Set a reasonable
serverSelectionTimeoutMSso a misconfigured or unreachable cluster fails fast instead of hanging your app for 30 seconds. - Call
pingor otherwise verify connectivity at startup so configuration mistakes surface immediately, not on the first user request. - Register a shutdown handler (e.g. on
SIGINT/SIGTERM) that callsclient.close()so your process doesn’t leak sockets or hang during redeploys. - Tune
maxPoolSizeto your actual concurrency needs — raising it blindly just shifts load onto the database server instead of solving a real bottleneck. - Prefer
mongodb+srv://connection strings for Atlas so the driver automatically tracks cluster topology changes without you updating hostnames.
Practice Exercises
- Write a small Node.js script that connects to a local or Atlas MongoDB instance, inserts one document into a
productscollection, then reads it back withfindOneand prints it. Make sure the client is closed at the end. - Refactor a hypothetical Express route that currently creates a new
MongoClientinside the handler so that it instead uses a single shared client created once at server startup. - Given a connection string with a password containing an
@character, determine why connecting fails and fix it usingencodeURIComponent. What error message would you expect to see if the special character isn’t escaped?
Summary
MongoClientfrom the official Node.js driver manages authentication, topology discovery, and a pool of reusable connections.- Create one
MongoClientper application and reuse it across every operation; never open a new client per request. mongodb+srv://connection strings use DNS to discover all cluster members from a single hostname;mongodb://requires listing hosts explicitly.- Key pool-tuning options include
maxPoolSize,minPoolSize, andserverSelectionTimeoutMS. - Always convert string IDs to
ObjectIdbefore querying by_id, and percent-encode special characters in credentials. - Close the client gracefully on process shutdown, but not after every individual operation.
