The official mongodb package is what every Node.js integration, including Mongoose, is built on. Knowing it well means understanding the connection pool you are sharing, what each operation returns, how BSON types cross the JavaScript boundary, and which errors you should expect. After this lesson you will be able to configure one client per process correctly, consume cursors efficiently, type collections with TypeScript, and handle driver errors deliberately.
MongoClient owns a pool of connections and is designed to be created once and shared for the life of the process. Creating a client per request exhausts server connections within minutes.
// db.js
import { MongoClient } from "mongodb";
export const client = new MongoClient(process.env.MONGODB_URI, {
maxPoolSize: 20, // default 100; size for your concurrency
serverSelectionTimeoutMS: 5000, // fail fast if the cluster is unreachable
appName: "orders-api" // shows up in server logs and Atlas metrics
});
export async function connectDb() {
await client.connect(); // optional since v4.7 (lazy), but fails fast at startup
await client.db("admin").command({ ping: 1 });
return client.db("shop");
}Call client.close() on shutdown so in-flight operations finish and sockets are released.
| Method | Resolves to |
|---|---|
| insertOne | { acknowledged, insertedId } |
| insertMany | { insertedCount, insertedIds } |
| updateOne / updateMany | { matchedCount, modifiedCount, upsertedId } |
| deleteOne / deleteMany | { deletedCount } |
| findOne | the document or null |
| findOneAndUpdate / findOneAndDelete | the document or null (driver 6.x) |
| find / aggregate | a cursor, not data |
Cursors are lazy. Chain options, then consume:
const cursor = products.find({ stock: { $gt: 0 } }).project({ name: 1, price: 1 }).sort({ price: 1 }).limit(50);
const list = await cursor.toArray(); // small results
for await (const doc of cursor) { /* ... */ } // streaming, constant memory
cursor.stream().pipe(transform); // Node stream for pipelinesimport { ObjectId, Decimal128, Long } from "mongodb";
const id = new ObjectId("67c1f0e8a1b2c3d4e5f60718");
ObjectId.isValid(req.params.id) // validate before querying
id.toHexString() // "67c1..." for JSON responses
Decimal128.fromString("19.99") // exact money
Long.fromString("9007199254740993") // beyond Number.MAX_SAFE_INTEGERJSON.stringify turns an ObjectId into its hex string and a Date into ISO text, which is fine for API responses. When data must round-trip losslessly (queues, caches), serialise with EJSON.stringify and EJSON.parse from the bson package.
The driver is typed generically. Declare the document shape once and every method is checked:
import type { WithId } from "mongodb";
interface Product { name: string; price: number; stock: number; tags?: string[] }
const products = client.db("shop").collection<Product>("products");
const doc: WithId<Product> | null = await products.findOne({ name: "Lamp" });
await products.updateOne({ name: "Lamp" }, { $inc: { stock: -1 } }); // typo in a field name fails to compileWithId<T> adds _id: ObjectId; OptionalId<T> describes insert payloads.
| Error class | When |
|---|---|
| MongoServerError | the server rejected the operation; inspect code (11000 = duplicate key) and errInfo (validation) |
| MongoBulkWriteError | some operations in insertMany/bulkWrite failed; see writeErrors and result |
| MongoNetworkError / MongoServerSelectionError | cluster unreachable or no suitable server within the timeout |
try {
await users.insertOne({ email });
} catch (err) {
if (err.code === 11000) return res.status(409).json({ error: "Email already registered" });
throw err;
}Retryable writes and reads are on by default, so a single transient network blip is retried once by the driver before you see an error. For deeper observability enable monitorCommands: true and listen to commandStarted, commandSucceeded and commandFailed events on the client.
MongoClient per request. Share one instance; the pool handles concurrency._id with a string received from a URL instead of new ObjectId(id).toArray() on unbounded queries, loading millions of documents into memory.await swallows errors.How many `MongoClient` instances should a typical Node.js service create?
MongoClient per process, configure the pool and timeouts, and close it on shutdown.find() and aggregate() return lazy cursors.for await or stream() and reserve toArray() for small sets.new ObjectId() and use Decimal128 for money.MongoServerError by code, MongoBulkWriteError by writeErrors, and rely on built-in retryable writes for transient failures.Next lesson: MongoDB with Node.js and Mongoose — add schemas and models on top of the driver with the most popular ODM.