Skip to content
Node.js nd database 5 min read

Using MongoDB with the Native Driver

MongoDB is a document database that stores rich, JSON-like records called documents, and the official mongodb package is the lowest-level, fully supported way to talk to it from Node.js. It speaks BSON natively, pools connections automatically, and returns documents as plain JavaScript objects — no ORM required. This page covers connecting, selecting databases and collections, the core CRUD operations, queries with filters, aggregation pipelines, and indexes.

Installing and connecting

Install the driver from npm. The single mongodb package includes everything: the client, BSON serialization, and the connection pool.

npm install mongodb

A MongoClient manages a pool of connections behind one connection string (a mongodb:// or mongodb+srv:// URI). You connect once at startup, reuse the client everywhere, and close it on shutdown. Creating a client per request is a common and costly mistake — the pool is meant to be shared.

import { MongoClient } from "mongodb";

const uri = process.env.MONGODB_URI ?? "mongodb://localhost:27017";
const client = new MongoClient(uri, { maxPoolSize: 20 });

await client.connect();
await client.db("admin").command({ ping: 1 });
console.log("Connected to MongoDB");

Output:

Connected to MongoDB

CommonJS users can write const { MongoClient } = require("mongodb"). The API is otherwise identical. Always call await client.close() during graceful shutdown to drain the pool.

Databases and collections

A client gives you a database handle with db(), and a database gives you a collection handle with collection(). Neither call hits the network — MongoDB creates the database and collection lazily on the first write. Use a TypeScript generic (or just a documented shape) to describe the document structure.

const db = client.db("shop");
const products = db.collection("products");

Documents have a _id field that MongoDB auto-generates as an ObjectId if you do not supply one. Import ObjectId when you need to query by id, since the string form must be wrapped.

import { ObjectId } from "mongodb";

const id = new ObjectId("507f1f77bcf86cd799439011");

CRUD operations

The collection object exposes the create, read, update, and delete methods. Each returns a result object describing what happened rather than the affected documents themselves.

// Create
const insert = await products.insertOne({
  name: "Mechanical Keyboard",
  price: 89.99,
  tags: ["peripherals", "input"],
  stock: 12,
});
console.log(`Inserted ${insert.insertedId}`);

// Read one
const item = await products.findOne({ name: "Mechanical Keyboard" });

// Update
const update = await products.updateOne(
  { _id: insert.insertedId },
  { $set: { price: 79.99 }, $inc: { stock: -1 } }
);
console.log(`Modified ${update.modifiedCount} document(s)`);

// Delete
const del = await products.deleteOne({ _id: insert.insertedId });
console.log(`Deleted ${del.deletedCount} document(s)`);

Output:

Inserted 66a1c2f4e9b3a10c4d8e7f21
Modified 1 document(s)
Deleted 1 document(s)

Use the bulk variants — insertMany, updateMany, deleteMany — when working with sets of documents. Update operators like $set, $inc, $push, and $pull modify fields in place without you having to read, mutate, and rewrite the whole document.

MethodPurposeKey result field
insertOne / insertManyAdd documentsinsertedId / insertedIds
findOne / findRead one / manydocument / cursor
updateOne / updateManyModify matching documentsmodifiedCount
replaceOneSwap a whole documentmodifiedCount
deleteOne / deleteManyRemove matching documentsdeletedCount

Queries with filters

find() returns a lazy cursor, not an array — nothing is fetched until you iterate it or call toArray(). Filters are plain objects; field comparisons use operators prefixed with $. You can chain sort, limit, skip, and project to shape the result set.

const cheapInStock = await products
  .find({ price: { $lt: 100 }, stock: { $gt: 0 } })
  .project({ name: 1, price: 1, _id: 0 })
  .sort({ price: -1 })
  .limit(5)
  .toArray();

console.log(cheapInStock);

Output:

[
  { name: 'Mechanical Keyboard', price: 79.99 },
  { name: 'USB Hub', price: 24.5 }
]

Common operators include $gt/$gte/$lt/$lte for ranges, $in/$nin for membership, $ne for inequality, $regex for pattern matching, and $and/$or for combining conditions. For large result sets, iterate the cursor with for await instead of toArray() so you never load everything into memory at once.

for await (const product of products.find({ tags: "peripherals" })) {
  console.log(product.name);
}

Aggregation basics

When you need grouping, joins, or computed fields, reach for the aggregation pipeline. aggregate() takes an array of stages, each transforming the stream of documents produced by the previous one. A typical pipeline filters with $match, groups with $group, then sorts.

const byTag = await products
  .aggregate([
    { $match: { stock: { $gt: 0 } } },
    { $unwind: "$tags" },
    { $group: { _id: "$tags", count: { $sum: 1 }, avgPrice: { $avg: "$price" } } },
    { $sort: { count: -1 } },
  ])
  .toArray();

console.log(byTag);

Output:

[
  { _id: 'peripherals', count: 14, avgPrice: 42.31 },
  { _id: 'input', count: 6, avgPrice: 58.9 }
]

$match early in the pipeline lets the engine use indexes and shrink the working set before expensive stages run.

Indexes

Without an index, every query scans the whole collection. createIndex builds one on a field (or compound set of fields); 1 means ascending order, -1 descending. Indexes are created once — typically at deploy time, not on every startup — and MongoDB skips the work if the index already exists.

await products.createIndex({ name: 1 }, { unique: true });
await products.createIndex({ price: -1, stock: 1 });

const plan = await products.find({ name: "USB Hub" }).explain("queryPlanner");
console.log(plan.queryPlanner.winningPlan.stage);

Output:

IXSCAN

An IXSCAN confirms the query used an index; a COLLSCAN means it scanned everything and probably needs one. Add a unique: true index to enforce uniqueness at the database level, and use a partial or TTL index for specialized cases like expiring sessions.

Best Practices

  • Create one MongoClient at startup, reuse it across the app, and close() it on shutdown — never connect per request.
  • Filter with operator objects ({ price: { $lt: 100 } }) and let the driver build the BSON; never string-concatenate queries.
  • Index the fields you query and sort by, and confirm usage with .explain() looking for IXSCAN over COLLSCAN.
  • Use update operators like $set and $inc instead of reading, mutating, and replacing whole documents.
  • Iterate large result sets with for await on the cursor rather than buffering everything via toArray().
  • Wrap string ids in new ObjectId(...) before matching the _id field.
  • Keep the connection URI and credentials in environment variables, not in source.
Last updated June 14, 2026
Was this helpful?