Reading Documents With the Node.js Driver
Learners will connect a Node.js script to MongoDB and perform insert/find operations using the official driver.
Reading Documents With the Node.js Driver is a free MongoDB Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the MongoDB Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Official Node.js Driver
The MongoDB Node.js driver (mongodb npm package) is the official, low-level client for connecting to MongoDB from Node.js. It is maintained by MongoDB Inc., supports the full MongoDB API, and ships with TypeScript type definitions.
The driver exposes a MongoClient class that manages a connection pool—a set of pre-established TCP connections reused across requests. This eliminates the overhead of creating a new connection for every operation. Most production Node.js apps share a single MongoClient instance for the lifetime of the process.
# Install the MongoDB Node.js driver
npm install mongodb
# TypeScript types are included - no @types/mongodb neededCreating a MongoClient and Connecting
Create a MongoClient with your connection string URI and call connect(). The connection is not established until connect() (or the first operation). Once connected, retrieve a database reference with client.db('dbName') and a collection reference with db.collection('collName').
Best practice: create the MongoClient once at application startup and export/inject it. Do NOT create a new client per request—this exhausts file descriptors and defeats the connection pool.
const { MongoClient } = require('mongodb');
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017';
const client = new MongoClient(uri);
async function main() {
await client.connect();
console.log('Connected to MongoDB');
const db = client.db('myapp');
const users = db.collection('users');
// ... perform operations ...
await client.close();
}
main().catch(console.error);Connection Pool and Options
The MongoClient maintains a connection pool of open TCP connections. When an operation needs to run, it borrows a connection from the pool, executes the operation, and returns the connection. Key pool options:
maxPoolSize(default 100): maximum simultaneous connectionsminPoolSize(default 0): keep-alive connections when idleconnectTimeoutMS: how long to wait for initial connectionserverSelectionTimeoutMS: how long to wait if no server is reachable
For most Node.js web servers, the default pool size of 100 is appropriate. Serverless functions (AWS Lambda) should use smaller pools.
const client = new MongoClient(uri, {
maxPoolSize: 20, // Max 20 simultaneous connections
minPoolSize: 5, // Keep 5 alive when idle
connectTimeoutMS: 5000, // 5s to establish initial connection
serverSelectionTimeoutMS: 5000 // 5s to select a server
});
// The client pool is shared across all operations
// Never create per-request clients!insertOne and insertMany in Node.js
All driver operations return Promises. Use async/await for clean, readable code. The insert methods accept documents as plain JavaScript objects—the driver automatically serializes them to BSON.
The result objects include acknowledged (boolean) and insertedId/insertedIds. TypeScript users can pass a generic type parameter to get typed documents: db.collection<User>('users').
const db = client.db('shop');
const products = db.collection('products');
// insertOne
const { insertedId } = await products.insertOne({
name: 'Wireless Mouse',
price: 29.99,
stock: 150,
createdAt: new Date()
});
console.log('Inserted product:', insertedId);
// insertMany
const { insertedCount } = await products.insertMany([
{ name: 'Keyboard', price: 49.99, stock: 80 },
{ name: 'Monitor', price: 299.99, stock: 25 }
]);
console.log('Inserted:', insertedCount);findOne in Node.js
collection.findOne(filter, options) returns a Promise that resolves to the matching document or null. The optional options object accepts projection, sort, and maxTimeMS.
A common pattern in web APIs: look up a resource by its URL parameter (the id string), convert it to an ObjectId for the query, and return 404 if null is returned. Always validate that the id string is a valid ObjectId format before constructing one—invalid strings throw synchronously.
const { ObjectId } = require('mongodb');
// Express route handler example
app.get('/products/:id', async (req, res) => {
let objId;
try {
objId = new ObjectId(req.params.id);
} catch {
return res.status(400).json({ error: 'Invalid product id' });
}
const product = await db.collection('products').findOne(
{ _id: objId },
{ projection: { name: 1, price: 1, stock: 1, _id: 0 } }
);
if (!product) return res.status(404).json({ error: 'Not found' });
res.json(product);
});find() and Cursor in Node.js
In Node.js, collection.find(filter, options) returns a FindCursor. You iterate it with for await...of, call .toArray() to load all results, or use .forEach(callback). All options (sort, limit, skip, projection) can be passed in the options object or chained as methods.
For API list endpoints, .toArray() is convenient—just ensure you always apply .limit(n) to prevent loading unbounded result sets. For data processing jobs (exports, migrations), stream with for await...of.
// List endpoint with pagination
app.get('/products', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = 20;
const skip = (page - 1) * limit;
const products = await db.collection('products')
.find({ stock: { $gt: 0 } })
.sort({ price: 1 })
.skip(skip)
.limit(limit)
.project({ name: 1, price: 1, _id: 1 })
.toArray();
res.json({ page, products });
});Error Handling Patterns
All driver operations can throw errors: network issues, authentication failures, write conflicts, validation errors. Use try/catch around all database calls in production code. Key error types:
MongoNetworkError: connectivity problem—retry logic may helpMongoServerErrorcode11000: duplicate key violationMongoServerErrorcode121: document failed schema validation
Build a withRetry wrapper for transient network errors. Do not retry validation or duplicate key errors—they indicate a logic bug, not a transient failure.
async function createUser(data) {
try {
const result = await db.collection('users').insertOne(data);
return { id: result.insertedId };
} catch (err) {
if (err.code === 11000) {
// Duplicate email
throw new Error('EMAIL_IN_USE');
}
if (err.code === 121) {
// Schema validation failed
throw new Error('INVALID_DATA');
}
// Network or other error - let it propagate
throw err;
}
}Handling ObjectId in API Responses
When returning MongoDB documents in a REST API response, ObjectId objects are serialized to a string representation by JSON.stringify, appearing as '64a2f3b1c9e7e12345678901'. Clients then send this string back as the ID in subsequent requests.
A common pattern is to transform documents in a mapping function: convert _id to id as a string, removing the MongoDB-specific underscore prefix that clients may find confusing. This also avoids leaking internal MongoDB implementation details to API consumers.
// Transform MongoDB document for API response
function toPublicUser(doc) {
const { _id, passwordHash, ...rest } = doc;
return {
id: _id.toString(), // ObjectId -> string
...rest // All other fields, minus passwordHash
};
}
const user = await db.collection('users').findOne({ email });
if (user) res.json(toPublicUser(user));
// Client sends 'id' string back:
// GET /users/64a2f3b1c9e7e12345678901
// Server converts: new ObjectId(req.params.id)Sharing the Client Across Modules
The recommended Node.js pattern is to initialize MongoClient once and share it across modules using a singleton pattern or dependency injection. A common approach is a db.js module that exports a connectDB() function and a getDB() accessor.
Call connectDB() once at application startup (in server.js or app.js). All route handlers and service modules call getDB() to get the database reference without creating new connections.
// db.js - singleton pattern
const { MongoClient } = require('mongodb');
let db;
async function connectDB() {
const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();
db = client.db('myapp');
console.log('MongoDB connected');
}
function getDB() {
if (!db) throw new Error('DB not initialized - call connectDB() first');
return db;
}
module.exports = { connectDB, getDB };
// Usage in a route:
// const { getDB } = require('./db');
// const db = getDB();
// await db.collection('users').find({}).toArray();Graceful Shutdown
When your Node.js process receives a shutdown signal (SIGTERM, SIGINT), close the MongoClient gracefully with client.close(). This flushes pending write buffers, closes open cursors, and releases TCP connections cleanly.
Without graceful shutdown, MongoDB may see an abrupt disconnection, trigger error handling on in-flight operations, and the connection pool on the server side remains open until the idle timeout fires. In high-traffic systems, many abrupt shutdowns can exhaust MongoDB's connection limit.
// Graceful shutdown handlers
const client = new MongoClient(uri);
await client.connect();
process.on('SIGINT', async () => {
console.log('Shutting down...');
await client.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('Received SIGTERM');
await client.close();
process.exit(0);
});
// Express:
const server = app.listen(3000);
process.on('SIGTERM', () => server.close(async () => {
await client.close();
}));TypeScript Integration
The MongoDB Node.js driver includes first-class TypeScript support. You can define an interface for your document shape and pass it as a generic to db.collection<MyType>(). The driver then provides type-safe results for findOne, find().toArray(), and more.
One subtlety: the TypeScript type must include _id?: ObjectId to match what MongoDB returns. When projecting out _id, TypeScript still includes it in the type—you may want to use WithId<T> or define separate input and output types for a fully type-safe implementation.
import { MongoClient, ObjectId } from 'mongodb';
// Define your document interface
interface User {
_id?: ObjectId;
name: string;
email: string;
age: number;
createdAt: Date;
}
const users = db.collection<User>('users');
// Fully typed result
const user = await users.findOne({ email: 'alice@test.com' });
// user is User | null
if (user) console.log(user.name.toUpperCase()); // TypeScript knows name is stringQuick Check
Test your understanding of MongoDB & NoSQL Databases concepts from this lesson.
Lesson Recap
In this lesson you learned: MongoClient manages a connection pool—create one instance at startup and share it across all modules to avoid connection overhead, all operations return Promises and should be awaited with try/catch for proper error handling, with error code 11000 indicating duplicate key violations, and TypeScript generics on collection<T>() provide type-safe document access with full IDE support. Next up we explore MongoDB's full suite of comparison and logical query operators to write precise, complex filters.
Frequently asked questions
Is the “Reading Documents With the Node.js Driver” lesson free?
Yes — the full text of “Reading Documents With the Node.js Driver” is free to read here on the web, and the MongoDB Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the MongoDB Academy course, upgrade to CoddyKit PRO.
What will I learn in “Reading Documents With the Node.js Driver”?
Learners will connect a Node.js script to MongoDB and perform insert/find operations using the official driver. You practise MongoDB Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start MongoDB Academy?
No prior experience is required. MongoDB Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Reading Documents With the Node.js Driver” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this MongoDB Academy lesson?
Yes. Every MongoDB Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- insertOne and insertMany
- findOne vs find: Cursors Explained
- Querying Nested Fields and Arrays
- Reading Documents With the Node.js Driver