# How to Get Data from MongoDB in Node.js: Best Practices for Performance and Security

> Learn how to get data from MongoDB in Node.js efficiently and securely. Explore best practices for performance, pagination, error handling, and secure connections.

- Repository: [mongodb/mongo](https://github.com/mongodb/mongo)
- Tags: best-practices
- Published: 2026-02-21

---

**Use a singleton MongoClient with TLS and SCRAM-SHA-256 authentication, query only indexed fields with projection to limit bandwidth, implement cursor-based pagination using the `_id` field instead of skip, and wrap all operations in try/catch blocks with exponential backoff retry logic for MongoNetworkError instances.**

When building production Node.js applications, understanding how to get data from MongoDB in Node.js efficiently and securely determines whether your service can handle high throughput without exposing sensitive credentials or wasting compute resources. The `mongodb/mongo` repository demonstrates these patterns through its own build infrastructure, where [`evergreen/streams_build_js_engine.sh`](https://github.com/mongodb/mongo/blob/main/evergreen/streams_build_js_engine.sh) pins specific Node.js versions for reproducible builds and [`README.md`](https://github.com/mongodb/mongo/blob/main/README.md) points developers to the official driver documentation. By adopting the connection pooling, indexing strategies, and error handling patterns used by the MongoDB engineering team, you can achieve sub-millisecond query latencies while maintaining ACID guarantees when necessary.

## Secure Connection Management and TLS Configuration

Always use the official `mongodb` npm driver and instantiate a single `MongoClient` at application startup to leverage built-in connection pooling. According to the MongoDB source code, the team automates Node.js installation in [`evergreen/streams_build_js_engine.sh`](https://github.com/mongodb/mongo/blob/main/evergreen/streams_build_js_engine.sh) (lines 8-15) to ensure consistent environments, illustrating the importance of pinning runtime versions in production containers. Store your connection URI in environment variables such as `MONGODB_URI` rather than hard-coding credentials, and enforce TLS encryption with certificate verification.

Configure the client with `tls: true` and `tlsAllowInvalidCertificates: false` to prevent man-in-the-middle attacks, and use SCRAM-SHA-256 or X.509 for authentication. The `MODULE.bazel` file (lines 112-115) declares `rules_nodejs` version 6.3.0, reflecting the repository’s commitment to stable Node.js tooling that you should mirror in your dependency management.

```javascript
// db.js – singleton MongoClient
import { MongoClient } from 'mongodb';

const uri = process.env.MONGODB_URI; // e.g. mongodb+srv://user:pwd@cluster0.mongodb.net/db?tls=true
if (!uri) {
  throw new Error('MONGODB_URI environment variable not set');
}

const client = new MongoClient(uri, {
  // Enable built‑in connection pool
  maxPoolSize: 20,
  // Strong read/write guarantees
  readConcern: { level: 'majority' },
  writeConcern: { w: 'majority', j: true },
  // Enforce TLS verification
  tls: true,
  tlsAllowInvalidCertificates: false,
});

await client.connect(); // runs once at startup
export const db = client.db(); // reuse across modules

```

## Optimize Queries with Indexes and Projection

Efficient data retrieval requires **index-driven queries** that avoid full collection scans. Model your queries to match existing indexes (e.g., `{ email: 1 }`), and validate execution plans using `explain()` during development. When fetching data, use the `projection` option to return only necessary fields, which reduces memory pressure on the driver and minimizes network bandwidth.

The [`README.md`](https://github.com/mongodb/mongo/blob/main/README.md) (lines 55-58) directs developers to the official driver documentation, which emphasizes that projection is critical for performance at scale. Avoid using `$where`, unanchored regular expressions, or `$text` searches on large unindexed fields, as these force O(N) document scans that degrade latency linearly with collection growth.

## Implement Efficient Pagination Without Skip

For large datasets, never use the `skip` method for pagination, as it requires the server to scan and discard all preceding documents, resulting in O(N) complexity. Instead, implement **range-based pagination** using the `_id` field or another indexed unique value with comparison operators like `$gt`.

This approach maintains O(log N) performance regardless of page depth. The following example demonstrates secure querying with projection, index utilization, and cursor-based pagination:

```javascript
import { db } from './db.js';

export async function getActiveUsers(pageSize, lastId) {
  try {
    const query = { isActive: true, _id: { $gt: lastId } };
    const opts = {
      projection: { _id: 1, name: 1, email: 1 }, // only needed fields
      sort: { _id: 1 },
      limit: pageSize,
    };
    const cursor = db.collection('users').find(query, opts);
    const results = await cursor.toArray();

    // Verify the plan uses the index (optional dev check)
    // const explain = await cursor.explain('executionStats');
    // console.log('Index used:', explain.executionStats.totalDocsExamined === 0);

    return results;
  } catch (err) {
    if (err instanceof MongoNetworkError) {
      // retry logic could go here
    }
    console.error('Failed to fetch users:', err);
    throw err; // propagate to caller
  }
}

```

## Handle Transient Errors with Retry Logic

Network partitions and replica set failovers trigger `MongoNetworkError` exceptions that require distinct handling from permanent logic errors. Wrap all database calls in `try/catch` blocks, detect transient failures by checking error types, and implement exponential backoff retry mechanisms. This resilience pattern ensures your application survives infrastructure blips without cascading failures.

The [`evergreen/streams_build_js_engine.sh`](https://github.com/mongodb/mongo/blob/main/evergreen/streams_build_js_engine.sh) script (lines 70-78) includes an `install_nodejs()` function that demonstrates automated error handling during dependency installation—a metaphor for the defensive coding required in production data access layers.

## Batch Operations and Transaction Safety

When inserting or updating multiple documents, use `bulkWrite()` to transmit operations in a single round-trip, significantly reducing network latency. Set `ordered: false` when operation sequence does not matter, allowing the server to parallelize work and continue processing despite individual document errors.

For operations requiring atomicity across multiple documents, use **multi-document transactions** sparingly. Keep transaction duration short to avoid lock contention, and always pass the session object to each operation within the transaction. The following example combines bulk writes with transactional safety:

```javascript
import { client, db } from './db.js';
import { MongoNetworkError } from 'mongodb';

export async function upsertOrders(orders) {
  const session = client.startSession();
  try {
    await session.withTransaction(async () => {
      const bulkOps = orders.map((o) => ({
        updateOne: {
          filter: { orderId: o.orderId },
          update: { $set: o },
          upsert: true,
        },
      }));
      await db.collection('orders')
        .bulkWrite(bulkOps, { ordered: false, session });
    }, {
      readConcern: { level: 'snapshot' },
      writeConcern: { w: 'majority' },
    });
  } catch (err) {
    if (err instanceof MongoNetworkError) {
      // implement exponential back‑off retry here
    }
    console.error('Bulk upsert failed:', err);
    throw err;
  } finally {
    await session.endSession();
  }
}

```

## Summary

- **Use a singleton MongoClient** with environment-variable-backed URIs and TLS enabled to ensure secure, pooled connections.
- **Query with projection** and enforce index usage to minimize bandwidth and CPU consumption.
- **Paginate using range queries** on indexed fields rather than `skip` to maintain constant-time performance at any page depth.
- **Distinguish transient errors** like `MongoNetworkError` from permanent failures and implement exponential backoff retries.
- **Leverage bulkWrite** with `ordered: false` for high-throughput updates, and reserve multi-document transactions only for cross-collection consistency requirements.

## Frequently Asked Questions

### Should I create a new MongoClient for every request in Node.js?

No. You should create one `MongoClient` instance at application startup and reuse it across all requests. The driver maintains an internal connection pool (configured via `maxPoolSize`) that handles concurrent operations efficiently. Creating new clients per request exhausts file descriptors and eliminates the performance benefits of connection reuse, as demonstrated by the singleton pattern used in MongoDB's own tooling.

### How do I prevent injection attacks when querying MongoDB from Node.js?

Always use the driver's built-in query operators and never concatenate user input into query strings. Use parameterized queries by passing values as variables rather than template literals, and sanitize inputs using libraries like `mongo-sanitize` to remove keys that start with `$`. Enabling **readConcern: "majority"** and **writeConcern: "majority"** also ensures you never read uncommitted data that could result from malicious manipulation.

### What is the most efficient way to paginate large result sets in MongoDB?

Use **cursor-based pagination** (range queries) on an indexed field such as `_id` instead of the `skip` method. Structure your query as `{ _id: { $gt: lastId } }` with `limit` and `sort`, which maintains O(log N) performance regardless of how deep you paginate. The `skip` method forces the server to scan and discard all preceding documents, resulting in linearly increasing latency as page numbers grow.

### When should I use multi-document transactions in Node.js applications?

Use multi-document transactions only when you need **atomic consistency across multiple documents or collections**. For single-document updates, rely on MongoDB's native atomicity. Keep transactions as short as possible—avoid long-running reads inside transactions—to minimize lock contention and performance impact. The `session.withTransaction()` helper simplifies retry logic for transient errors during commit.