mrkeyoor.com_
Wed 05 Aug 05:04 UTC
npmDataupdated 05 Aug 2026

mongodb

The official MongoDB driver for Node.js, maintained by MongoDB Inc. It is the low-level client: you get a connection pool, typed CRUD methods on collections, aggregation pipelines, transactions, change streams, and BSON handling, with first-class TypeScript generics for your document shapes. There is no schema layer, no validation, no model classes; documents go in and come out as plain objects with BSON types like ObjectId and Decimal128. Every higher-level MongoDB tool in the Node ecosystem (Mongoose included) sits on top of this driver, so learning its API pays off even if you add an ODM later.

Verdict

If you use MongoDB from Node, this driver is not optional, the only question is whether you put Mongoose on top. Use it bare when you want control and few dependencies; add an ODM when your team wants schemas and hooks.

API stability4/5Follows semver strictly and documents every breaking change with per-major upgrade guides, but majors arrive often (v7 is current, released within the last year of majors v5/v6) and each one raises the Node floor or removes callbacks-era API.
Docs4/5The official docs site covers fundamentals, usage examples, and an API reference, plus MongoDB University courses; quality is high but content is spread across docs, API site, and upgrade notes in the repo.
Maintenance5/5Corporate-backed team with daily activity (pushed 2026-08-03), only 31 open GitHub issues because triage happens in JIRA, signed releases with npm provenance, and a published semver policy.
Ecosystem4/514.8M weekly downloads and it underpins Mongoose and most MongoDB tooling in Node; the ecosystem is deep but MongoDB-shaped, so it is smaller than the SQL world's collective tooling.

Use it if

  • You want direct control over queries, indexes, and aggregation pipelines without an ODM translating for you
  • You already validate data at the application boundary (zod, JSON Schema, or MongoDB server-side validation) and do not need Mongoose's schema layer
  • You need driver-level features quickly: change streams, transactions, client-side field level encryption, or the newest server capabilities land here first
  • You care about dependency weight: the driver has three runtime deps, all MongoDB-owned
Skip it if

Setup reality

npm install mongodb is clean: three dependencies and no native builds by default. The catch is the optional peer dependency list (kerberos, mongodb-client-encryption, @mongodb-js/zstd, snappy, socks, gcp-metadata, aws credential providers); npm prints noisy warnings for features you may never use, and the moment you do need Kerberos auth or field-level encryption you are compiling native modules. Budget time for connection-string tuning too: pool sizes, serverSelectionTimeoutMS, and TLS options are where most production incidents hide. Node >=20.19.0 is enforced for v7.

Patterns

Connect and reuse one clientconnect

import { MongoClient } from 'mongodb'

const client = new MongoClient(process.env.MONGODB_URI)
await client.connect()
const db = client.db('shop')
const users = db.collection('users')

Create one MongoClient per process and reuse it; it manages a pool internally, so per-request clients exhaust connections.

Type documents with genericstyped-collection

import { ObjectId } from 'mongodb'

interface User {
  _id?: ObjectId
  email: string
  createdAt: Date
}

const users = db.collection<User>('users')

Types are compile-time only; the driver does zero runtime validation of what you insert.

Insert one and manyinsert

const one = await users.insertOne({ email: 'a@b.com', createdAt: new Date() })
console.log(one.insertedId)

await users.insertMany([
  { email: 'c@d.com', createdAt: new Date() },
  { email: 'e@f.com', createdAt: new Date() },
], { ordered: false })

ordered: false keeps inserting past duplicate-key errors instead of stopping at the first failure.

Query with filter, projection, sortfind-query

const recent = await users
  .find({ createdAt: { $gte: new Date('2026-01-01') } })
  .project({ email: 1 })
  .sort({ createdAt: -1 })
  .limit(20)
  .toArray()

find() returns a lazy cursor; nothing hits the server until toArray(), for-await, or next().

Look up by _idfind-by-id

import { ObjectId } from 'mongodb'

const user = await users.findOne({ _id: new ObjectId(idString) })

String ids never match ObjectId fields; forgetting the wrapper is the classic silent empty-result bug.

Update with $set and upsertupdate-upsert

await users.updateOne(
  { email: 'a@b.com' },
  { $set: { plan: 'pro' }, $setOnInsert: { createdAt: new Date() } },
  { upsert: true },
)

Passing a plain object without update operators throws; replaceOne is the API for full-document replacement.

Atomically update and return the docfind-one-and-update

const updated = await users.findOneAndUpdate(
  { _id: userId },
  { $inc: { credits: -1 } },
  { returnDocument: 'after' },
)

Since v6 this returns the document directly (or null); older code expecting result.value breaks.

Run an aggregation pipelineaggregate

const totals = await orders.aggregate([
  { $match: { status: 'paid' } },
  { $group: { _id: '$userId', total: { $sum: '$amount' } } },
  { $sort: { total: -1 } },
  { $limit: 10 },
]).toArray()

Put $match first so indexes apply; stages after $group run on unindexed in-memory data.

Create indexes at startupcreate-index

await users.createIndex({ email: 1 }, { unique: true })
await orders.createIndex({ userId: 1, createdAt: -1 })

createIndex is a no-op if the identical index exists, so calling it on boot is safe and common.

Multi-document transactiontransactions

const session = client.startSession()
try {
  await session.withTransaction(async () => {
    await accounts.updateOne({ _id: from }, { $inc: { balance: -100 } }, { session })
    await accounts.updateOne({ _id: to }, { $inc: { balance: 100 } }, { session })
  })
} finally {
  await session.endSession()
}

Every operation inside must pass { session } or it silently runs outside the transaction; requires a replica set.

React to live data changeschange-streams

const stream = orders.watch([
  { $match: { operationType: 'insert' } },
])
for await (const change of stream) {
  console.log('new order', change.fullDocument._id)
}

Change streams need a replica set or Atlas; on a standalone mongod watch() errors immediately.

Batch mixed writesbulk-write

await users.bulkWrite([
  { insertOne: { document: { email: 'x@y.com', createdAt: new Date() } } },
  { updateOne: { filter: { email: 'a@b.com' }, update: { $set: { plan: 'free' } } } },
  { deleteOne: { filter: { email: 'old@z.com' } } },
])

One round trip for many writes; with ordered: false the server executes what it can and reports all errors together.

Alternatives

PackageRegistryPick it when
mongoosenpmYou want schemas, validation, middleware, and populate() on top of MongoDB and accept the abstraction cost
prismanpmYou want a typed data layer with schema migrations across databases; its MongoDB support covers common CRUD but not the full aggregation surface
postgresnpmYour data is relational; this driver plus PostgreSQL is a simpler long-term bet than modelling relations in MongoDB