1. Introduction đą
MongoDB is a document-oriented, NoSQL database that stores data as flexible, JSON-like documents instead of rigid rows and columns. When paired with Node.js, it becomes one of the most popular stacks for building scalable, modern web applications â commonly referred to as part of the MERN/MEAN stack.
In this tutorial, you will learn how to connect to MongoDB from a Node.js application, perform CRUD operations, work with aggregation pipelines, handle transactions, and apply production-grade best practices.
Information
Why MongoDB with Node.js? đ¤
Both MongoDB and Node.js speak JSON/BSON natively, which removes the need for an ORM translation layer that relational databases typically require. This makes the developer experience seamless.
- Schema flexibilityâ documents in the same collection can have different fields.
- JSON-nativeâ data maps directly to JavaScript objects.
- Horizontal scalabilityâ built-in sharding support.
- Rich queryingâ powerful aggregation framework.
2. MongoDB Node.js Driver đ
The official mongodb package is the low-level driver maintained by MongoDB Inc. It provides direct access to the database without any additional abstraction layer, unlike Mongoose, which is an ODM built on top of it.
The mongodb package gives you full control and closely mirrors the shell API. Install it with:
terminal
npm install mongodbMongoose adds schemas, validation, and middleware/hooks on top of the native driver, at the cost of some flexibility and a slight performance overhead.
terminal
npm install mongooseTip
3. Connecting to MongoDB đ
Connecting involves creating a MongoClient instance, calling connect(), and obtaining a reference to your target database.
connect.js
const { MongoClient } = require('mongodb');
const uri = 'mongodb://127.0.0.1:27017';
const client = new MongoClient(uri);
async function main() {
await client.connect();
console.log('Connected successfully! đ');
const db = client.db('shopDB');
return db;
}
main().catch(console.error);4. Connection Strings đ
A MongoDB connection string (URI) encodes host, port, credentials, database name, and options in a single string.
| Format | Example | Use Case |
|---|---|---|
| Standard | mongodb://host:port/db | Local or single-node deployments |
| SRV | mongodb+srv://cluster.mongodb.net | MongoDB Atlas / replica sets |
.env
MONGO_URI=mongodb+srv://<username>:<password>@cluster0.mongodb.net/shopDB?retryWrites=true&w=majorityWarning
Common URI Options âī¸
- retryWrites=trueâ automatically retries failed write operations.
- w=majorityâ write concern requiring acknowledgment from a majority of replica set members.
- maxPoolSizeâ controls the connection pool size (see Section 17).
5. MongoClient đ§
The MongoClient class manages the underlying connection pool and is designed to be instantiated once and reused throughout your application's lifetime.
client.js
const { MongoClient } = require('mongodb');
const client = new MongoClient(process.env.MONGO_URI, {
maxPoolSize: 10,
});
async function connectDB() {
try {
await client.connect();
await client.db('admin').command({ ping: 1 });
console.log('Pinged your deployment. Connection successful! â
');
} catch (err) {
console.error('Connection failed â', err);
}
}
module.exports = { client, connectDB };Important
6. Database Instances đī¸
Once connected, use client.db(name) to obtain a Db object, which acts as an entry point to all collections within that database.
db-instance.js
const db = client.db('shopDB');
// List all collections in this database
const collections = await db.listCollections().toArray();
console.log(collections.map(c => c.name));- If the database name is omitted, it defaults to the one specified in the connection string.
- Databases are created lazilyâ they only persist once data is written to them.
7. Collection Instances đ
A collection is analogous to a table in relational databases, but without an enforced schema. Access one via db.collection(name).
collection-instance.js
const usersCollection = db.collection('users');
// Create with explicit validation rules
await db.createCollection('orders', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['itemName', 'quantity'],
properties: {
itemName: { bsonType: 'string' },
quantity: { bsonType: 'int', minimum: 1 },
},
},
},
});Note
8. CRUD Operations đ ī¸
CRUD stands for Create, Read, Update, Deleteâ the four fundamental operations for interacting with data.
Create â
create.js
// Insert a single document
const result = await usersCollection.insertOne({
name: 'Alice',
email: 'alice@example.com',
age: 28,
});
console.log('Inserted ID:', result.insertedId);
// Insert multiple documents
await usersCollection.insertMany([
{ name: 'Bob', age: 34 },
{ name: 'Carol', age: 25 },
]);Read đ
read.js
const user = await usersCollection.findOne({ name: 'Alice' });
const allUsers = await usersCollection.find({}).toArray();Update âī¸
update.js
await usersCollection.updateOne(
{ name: 'Alice' },
{ $set: { age: 29 } }
);
await usersCollection.updateMany(
{ age: { $lt: 30 } },
{ $inc: { age: 1 } }
);Delete đī¸
delete.js
await usersCollection.deleteOne({ name: 'Bob' });
await usersCollection.deleteMany({ age: { $gt: 60 } });| Operation | Single Document | Multiple Documents |
|---|---|---|
| Create | insertOne | insertMany |
| Update | updateOne | updateMany |
| Delete | deleteOne | deleteMany |
9. Querying Documents đ
MongoDB's query language uses operators prefixed with $ to build expressive filters.
Comparison Operators
query-comparison.js
// Find users older than 25 and younger than 40
const results = await usersCollection.find({
age: { $gt: 25, $lt: 40 },
}).toArray();Logical Operators
query-logical.js
const results = await usersCollection.find({
$or: [{ name: 'Alice' }, { name: 'Carol' }],
}).toArray();Projection, Sorting & Pagination
query-advanced.js
const results = await usersCollection
.find({}, { projection: { name: 1, email: 1, _id: 0 } })
.sort({ age: -1 })
.skip(10)
.limit(5)
.toArray();Best Practice
10. Aggregation đ
The aggregation pipeline processes documents through a sequence of stages, each transforming the data â similar to a Unix pipe.
aggregation.js
const results = await db.collection('orders').aggregate([
{ $match: { status: 'completed' } },
{ $group: { _id: '$customerId', total: { $sum: '$amount' } } },
{ $sort: { total: -1 } },
{ $limit: 10 },
]).toArray();- $matchâ filters documents (like a query).
- $groupâ groups documents and computes aggregates.
- $projectâ reshapes documents, including/excluding fields.
- $lookupâ performs a left outer join with another collection.
11. Transactions đ
Transactions allow multiple operations across one or more documents/collections to execute atomicallyâ either all succeed or all are rolled back.
transactions.js
const session = client.startSession();
try {
await session.withTransaction(async () => {
const accounts = db.collection('accounts');
await accounts.updateOne(
{ _id: 'A' }, { $inc: { balance: -100 } }, { session }
);
await accounts.updateOne(
{ _id: 'B' }, { $inc: { balance: 100 } }, { session }
);
});
console.log('Transaction committed successfully â
');
} finally {
await session.endSession();
}Caution
12. Sessions đĢ
A ClientSession tracks a logical sequence of operations that need to be causally related â required for transactions, and useful for causal consistency.
sessions.js
const session = client.startSession({
defaultTransactionOptions: {
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' },
},
});
// Always end sessions when done
await session.endSession();Reference
13. Bulk Operations đĻ
When performing many writes, bulkWrite() batches operations into a single round-trip, significantly improving throughput.
bulk-operations.js
const result = await usersCollection.bulkWrite([
{ insertOne: { document: { name: 'Dave', age: 40 } } },
{ updateOne: { filter: { name: 'Alice' }, update: { $set: { age: 30 } } } },
{ deleteOne: { filter: { name: 'Bob' } } },
]);
console.log(`Inserted: ${result.insertedCount}, Modified: ${result.modifiedCount}`);Tip
14. Change Streams đĄ
Change streams let your application react in real time to data changes, without polling, by tailing the replication oplog.
change-streams.js
const changeStream = usersCollection.watch();
changeStream.on('change', (change) => {
console.log('Change detected đ:', change.operationType);
});
// Filter for specific operation types
const pipeline = [{ $match: { operationType: 'insert' } }];
const insertStream = usersCollection.watch(pipeline);Information
15. GridFS đŧī¸
GridFS is a specification for storing and retrieving files that exceed the 16MB document size limit, by splitting them into chunks.
gridfs.js
const { GridFSBucket } = require('mongodb');
const fs = require('fs');
const bucket = new GridFSBucket(db, { bucketName: 'uploads' });
// Upload a file
fs.createReadStream('./photo.jpg')
.pipe(bucket.openUploadStream('photo.jpg'))
.on('finish', () => console.log('Upload complete đ¤'));
// Download a file
bucket.openDownloadStreamByName('photo.jpg')
.pipe(fs.createWriteStream('./downloaded.jpg'));Example
16. Error Handling â ī¸
Robust error handling distinguishes between driver-level errors, network errors, and validation errors.
error-handling.js
const { MongoServerError } = require('mongodb');
try {
await usersCollection.insertOne({ _id: 1, name: 'Alice' });
await usersCollection.insertOne({ _id: 1, name: 'Duplicate' });
} catch (err) {
if (err instanceof MongoServerError && err.code === 11000) {
console.error('Duplicate key error đĢ:', err.message);
} else {
console.error('Unexpected error:', err);
}
}Warning
17. Connection Pooling đ
The driver maintains a pool of reusable connections per MongoClient instance rather than opening a new socket for every query.
pooling.js
const client = new MongoClient(uri, {
maxPoolSize: 20, // maximum simultaneous connections
minPoolSize: 5, // connections kept alive even when idle
maxIdleTimeMS: 30000,
});| Option | Purpose |
|---|---|
| maxPoolSize | Caps concurrent connections per client |
| minPoolSize | Keeps a baseline of warm connections |
| maxIdleTimeMS | Closes connections idle beyond this duration |
Best Practice
18. Environment Variables đ
Sensitive configuration â like connection URIs and credentials â should live in environment variables, never in source code.
.env
MONGO_URI=mongodb+srv://user:pass@cluster0.mongodb.net/shopDB
NODE_ENV=productiondb.js
require('dotenv').config();
const { MongoClient } = require('mongodb');
const client = new MongoClient(process.env.MONGO_URI);Danger
19. Async/Await âŗ
Nearly every driver method returns a Promise, making async/await the cleanest way to write sequential, readable database code.
async-await.js
async function getActiveUsers() {
try {
const users = await usersCollection
.find({ status: 'active' })
.toArray();
return users;
} catch (err) {
console.error('Query failed:', err);
throw err;
}
}Tip
parallel-queries.js
const [users, orders] = await Promise.all([
usersCollection.find({}).toArray(),
ordersCollection.find({}).toArray(),
]);20. Performance Optimization đ
Indexing
indexing.js
// Single field index
await usersCollection.createIndex({ email: 1 }, { unique: true });
// Compound index
await usersCollection.createIndex({ age: 1, name: -1 });Query Profiling
explain.js
const explanation = await usersCollection
.find({ age: { $gt: 25 } })
.explain('executionStats');
console.log(explanation.executionStats.totalDocsExamined);- Use .explain() to detect full collection scans.
- Limit fields returned using projections.
- Avoid unbounded $regex queries without anchors.
- Batch writes using bulkWrite() (see Section 13).
21. Security Best Practices đĄī¸
- Enable authentication and use RBAC with least-privilege roles.
- Always connect via TLS/SSL in production.
- Restrict network access with IP allow-lists or VPC peering.
- Store credentials in environment variables or a secrets manager, never in code.
- Sanitize user input to prevent NoSQLi attacks (e.g., avoid passing raw objects from request bodies directly into queries).
sanitize-example.js
// â Dangerous: raw user input passed directly
const user = await usersCollection.findOne(req.body);
// â
Safer: explicitly whitelist expected fields
const user = await usersCollection.findOne({
email: String(req.body.email),
});Danger
22. Best Practices â
- Reuse a single MongoClient instance app-wide.
- Always close connections gracefully on process shutdown.
- Use schema validation at the collection level even in a schema-less database.
- Prefer async/await over callback-based code.
- Log slow queries and monitor with tools like MongoDB Atlas Performance Advisor.
graceful-shutdown.js
process.on('SIGINT', async () => {
await client.close();
console.log('MongoDB connection closed đ');
process.exit(0);
});23. Common Mistakes đĢ
| Mistake | Why It's a Problem |
|---|---|
| Creating a new MongoClient per request | Exhausts connections and degrades performance |
| Not awaiting connect() | Leads to race conditions and intermittent failures |
| Ignoring indexes | Causes slow, full collection scans |
| Passing raw request bodies into queries | Opens the door to NoSQL injection |
| Not handling ObjectId casting | Causes silent query mismatches |
Caution
24. Frequently Asked Questions â
Question
Answer
Question
Answer
Question
Answer
25. Summary đ
Summary
- Official Docs: MongoDB Node.js Driver Documentation
- Free Learning: MongoDB University