MongoDB with Node.js
Learn how to use MongoDB in your Node.js applications! Master the MongoDB driver, Mongoose ODM, indexing, aggregation, and schema design patterns for production-ready apps.
Prerequisites
Make sure you understand MongoDB basics and CRUD operations. If you haven't, check out the first!
While the MongoDB shell is great for learning, real applications need to interact with MongoDB programmatically. The MongoDB Node.js driver allows you to perform all database operations directly from your JavaScript code.
Installation
# Install the MongoDB drivernpm install mongodb# Optional: Install dotenv for environment variablesnpm install dotenv
Basic Connection
The first step is connecting to your MongoDB database. You create a MongoClient and connect to your database server.
const { MongoClient } = require('mongodb');// Connection string for local MongoDBconst uri = 'mongodb://localhost:27017';// Or for MongoDB Atlas (cloud)// const uri = 'mongodb+srv://username:password@cluster.mongodb.net/myDatabase';const client = new MongoClient(uri);async function run() {try {// Connect to MongoDBawait client.connect();console.log('Connected to MongoDB!');// Access a databaseconst db = client.db('schoolDB');// Access a collectionconst students = db.collection('students');// Now you can perform operations!const result = await students.findOne({ name: 'Sarah' });console.log(result);} catch (error) {console.error('Error:', error);} finally {// Always close the connection when doneawait client.close();}}run();
Using Environment Variables
Never hardcode your connection string! Store it in environment variables for security.
# Create a .env file in your project rootMONGODB_URI=mongodb://localhost:27017/schoolDB
// app.jsrequire('dotenv').config();const { MongoClient } = require('mongodb');// Load connection string from environment variableconst uri = process.env.MONGODB_URI;const client = new MongoClient(uri);async function run() {try {await client.connect();const db = client.db(); // Uses database from URI// Your code here...} finally {await client.close();}}run();
CRUD Operations with Node.js Driver
All the CRUD operations you learned in the shell work the same way in Node.js! The syntax is almost identical.
const { MongoClient } = require('mongodb');const uri = 'mongodb://localhost:27017';const client = new MongoClient(uri);async function crudExamples() {await client.connect();const db = client.db('schoolDB');const students = db.collection('students');// CREATE - Insert documentsawait students.insertOne({name: 'John Doe',age: 20,major: 'Computer Science'});await students.insertMany([{ name: 'Jane Smith', age: 21, major: 'Mathematics' },{ name: 'Bob Johnson', age: 19, major: 'Physics' }]);// READ - Find documentsconst student = await students.findOne({ name: 'John Doe' });console.log(student);const allStudents = await students.find({}).toArray();console.log(allStudents);const csStudents = await students.find({major: 'Computer Science'}).toArray();// UPDATE - Modify documentsawait students.updateOne({ name: 'John Doe' },{ $set: { age: 21 } });await students.updateMany({ major: 'Computer Science' },{ $set: { department: 'CS Dept' } });// DELETE - Remove documentsawait students.deleteOne({ name: 'Bob Johnson' });await students.deleteMany({ age: { $lt: 18 } });await client.close();}crudExamples();
Important: .toArray()
When using find() in Node.js, it returns a cursor (not the actual data). You need to call .toArray() to get the actual documents as an array. With findOne(), you get the document directly.
Mongoose is an Object Data Modeling (ODM) library that makes working with MongoDB easier. It adds structure, validation, and many helpful features on top of the MongoDB driver.
Why Use Mongoose?
- Schema Validation - Define structure and rules for your data
- Type Casting - Automatic conversion to correct data types
- Middleware - Run code before/after database operations
- Query Building - Chainable methods for complex queries
- Better for Large Apps - More structure and organization
Installation
npm install mongoose
Connecting with Mongoose
const mongoose = require('mongoose');// Connect to MongoDBmongoose.connect('mongodb://localhost:27017/schoolDB').then(() => console.log('Connected to MongoDB')).catch(err => console.error('Connection error:', err));// Or using async/awaitasync function connectDB() {try {await mongoose.connect('mongodb://localhost:27017/schoolDB');console.log('Connected to MongoDB');} catch (error) {console.error('Error:', error);}}connectDB();
Defining a Schema
A schema defines the structure of your documents. It's like a blueprint that tells MongoDB what fields your documents should have and what types they should be.
const mongoose = require('mongoose');// Define a schemaconst studentSchema = new mongoose.Schema({name: {type: String,required: true, // This field is requiredtrim: true // Remove whitespace},email: {type: String,required: true,unique: true, // Must be uniquelowercase: true // Convert to lowercase},age: {type: Number,min: 0, // Minimum valuemax: 120 // Maximum value},major: String, // Simple field definitiongpa: Number,enrollmentDate: {type: Date,default: Date.now // Default to current date},hobbies: [String], // Array of stringsisActive: {type: Boolean,default: true}}, {timestamps: true // Adds createdAt and updatedAt automatically});// Create a model from the schemaconst Student = mongoose.model('Student', studentSchema);module.exports = Student;
CRUD with Mongoose
Mongoose provides cleaner, more intuitive methods for CRUD operations:
const Student = require('./models/Student');async function mongooseExamples() {// CREATE// Method 1: Using create()const student1 = await Student.create({name: 'Alice',email: 'alice@university.edu',age: 20,major: 'Computer Science'});// Method 2: Using new and save()const student2 = new Student({name: 'Bob',email: 'bob@university.edu',age: 21});await student2.save();// READ// Find allconst allStudents = await Student.find();// Find with criteriaconst csStudents = await Student.find({ major: 'Computer Science' });// Find oneconst alice = await Student.findOne({ name: 'Alice' });// Find by IDconst student = await Student.findById('507f1f77bcf86cd799439011');// With query methods (chainable)const results = await Student.find({ age: { $gte: 20 } }).sort({ name: 1 }).limit(10).select('name email'); // Only return these fields// UPDATE// Find and update (returns updated document)const updated = await Student.findOneAndUpdate({ name: 'Alice' },{ age: 21 },{ new: true } // Return updated document);// Update by IDawait Student.findByIdAndUpdate('507f1f77bcf86cd799439011',{ $inc: { age: 1 } });// Update manyawait Student.updateMany({ major: 'Computer Science' },{ $set: { department: 'CS' } });// DELETEawait Student.findOneAndDelete({ name: 'Bob' });await Student.findByIdAndDelete('507f1f77bcf86cd799439011');await Student.deleteMany({ age: { $lt: 18 } });// Count documentsconst count = await Student.countDocuments({ major: 'Computer Science' });}mongooseExamples();
Mongoose vs Native Driver
Choose based on your needs:
- Use Mongoose if: You want structure, validation, and are building a larger app
- Use Native Driver if: You want maximum flexibility and minimal overhead
Indexes are special data structures that make finding documents much faster. Without indexes, MongoDB has to scan every single document to find what you're looking for. With indexes, it can jump directly to the right documents.
Think of it Like a Book Index
Imagine looking for the word "database" in a 500-page book. Without an index, you'd have to read every page. With an index at the back of the book, you can jump straight to the right pages. That's exactly what database indexes do!
When to Use Indexes
- Fields you frequently search by (e.g., email, username)
- Fields you sort by (e.g., createdAt, price)
- Fields used in complex queries
Creating Indexes in Mongoose
const studentSchema = new mongoose.Schema({email: {type: String,unique: true, // Automatically creates a unique indexindex: true // Creates a regular index},name: {type: String,index: true // Good for searching by name},createdAt: {type: Date,index: true // Good for sorting by date}});// Create a compound index (index on multiple fields)studentSchema.index({ major: 1, age: -1 });// 1 = ascending, -1 = descending// Text index for full-text searchstudentSchema.index({ bio: 'text' });
Creating Indexes with Native Driver
const students = db.collection('students');// Single field indexawait students.createIndex({ email: 1 });// Unique indexawait students.createIndex({ email: 1 }, { unique: true });// Compound indexawait students.createIndex({ major: 1, age: -1 });// Text indexawait students.createIndex({ bio: 'text' });// View all indexesconst indexes = await students.indexes();console.log(indexes);
Index Trade-offs
Indexes make reads faster but writes slower (because the index needs to be updated). They also take up disk space. Don't create indexes on every field - only on fields you actually search/sort by frequently!
Aggregation allows you to process and analyze your data. Think of it like advanced filtering, grouping, and calculations - similar to what you might do in Excel with pivot tables or SQL with GROUP BY.
What is Aggregation?
Aggregation is like a pipeline where data flows through multiple stages. Each stage transforms or filters the data, and the output of one stage becomes the input of the next.
Basic Aggregation Example
Let's count how many students are in each major:
const Student = require('./models/Student');// Count students by majorconst majorCounts = await Student.aggregate([{$group: {_id: '$major', // Group by major fieldcount: { $sum: 1 } // Count documents in each group}},{$sort: { count: -1 } // Sort by count (descending)}]);// Result:// [// { _id: 'Computer Science', count: 45 },// { _id: 'Mathematics', count: 32 },// { _id: 'Physics', count: 28 }// ]
Common Aggregation Stages
// Multi-stage aggregation pipelineconst results = await Student.aggregate([// Stage 1: FILTER - Match only active students{$match: {isActive: true,age: { $gte: 18 }}},// Stage 2: GROUP - Calculate statistics by major{$group: {_id: '$major',averageAge: { $avg: '$age' },totalStudents: { $sum: 1 },maxGPA: { $max: '$gpa' },minGPA: { $min: '$gpa' }}},// Stage 3: SORT - Sort by average age{$sort: { averageAge: -1 }},// Stage 4: LIMIT - Get top 5{$limit: 5},// Stage 5: PROJECT - Reshape the output{$project: {major: '$_id',avgAge: { $round: ['$averageAge', 1] },students: '$totalStudents',_id: 0}}]);
Common Aggregation Operators
$match— Filter documents (like find())$group— Group documents and calculate aggregates$sort— Sort documents$limit— Limit number of results$skip— Skip documents$project— Reshape documents, select fields$lookup— Join with another collection$unwind— Deconstruct arrays
Even though MongoDB is flexible, good schema design is crucial for performance. The main decision is whether to embed related data or reference it.
Pattern 1: Embedding (Nesting)
Store related data inside the same document. Good when data is accessed together.
// User with embedded address{_id: ObjectId("123"),name: "Alice",email: "alice@example.com",address: {street: "123 Main St",city: "New York",zipCode: "10001",country: "USA"},hobbies: ["reading", "coding", "gaming"]}// Use embedding when:// - Related data is always accessed together// - One-to-one or one-to-few relationships// - Data doesn't grow unbounded// - Data isn't shared across documents
Advantage: Get all data in one query (fast!)
Disadvantage: Duplicated data if the same address is used by multiple users
Pattern 2: Referencing (Linking)
Store related data in separate collections and link them with IDs. Good for data that's shared or grows large.
// User collection{_id: ObjectId("123"),name: "Alice",email: "alice@example.com"}// Posts collection (references user){_id: ObjectId("456"),title: "Learning MongoDB",content: "MongoDB is awesome...",authorId: ObjectId("123"), // Reference to usercreatedAt: ISODate("2024-01-15")}// Use referencing when:// - Data is large or grows unbounded// - One-to-many or many-to-many relationships// - Data is shared across multiple documents// - Data is updated frequently
Advantage: No duplication, easier to update
Disadvantage: Requires multiple queries (slower)
Example: Blog Schema Design
// User Schemaconst userSchema = new mongoose.Schema({name: String,email: String,bio: String});// Post Schema (with references and embedding)const postSchema = new mongoose.Schema({title: String,content: String,author: {type: mongoose.Schema.Types.ObjectId,ref: 'User' // Reference to User model},comments: [{ // Embed comments (one-to-few)user: String,text: String,date: Date}],tags: [String], // Embed tags (simple data)likes: Number,createdAt: Date});// Using populate to get referenced dataconst posts = await Post.find().populate('author', 'name email') // Get author details.exec();
Design Guidelines
- Consider your query patterns - how will you access the data?
- Embed for high read-to-write ratio
- Reference for frequently updated data
- Document size limit is 16MB
- Avoid deeply nested structures (>100 levels)
1. Connection Management
- Reuse connections - don't create a new connection for every query
- Use connection pooling (default in most drivers)
- Always close connections when shutting down your app
2. Error Handling
async function safeOperation() {try {const result = await Student.findOne({ email: 'test@example.com' });if (!result) {throw new Error('Student not found');}return result;} catch (error) {console.error('Database error:', error.message);throw error; // Re-throw for caller to handle}}
3. Security
- Never expose connection strings in code
- Use environment variables for sensitive data
- Add
.envto.gitignore - Use authentication in production
- Limit network access (use IP whitelist)
4. Performance Tips
- Create indexes on frequently queried fields
- Use projection to fetch only needed fields
- Use
lean()in Mongoose for faster queries when you don't need Mongoose features - Limit and paginate large result sets
- Use aggregation for complex data processing
5. Data Validation
- Define schemas with proper validation rules
- Use required fields where appropriate
- Add min/max constraints for numbers
- Use enum for fields with limited values
- Validate data before saving
You've Mastered MongoDB!
Congratulations! You now know how to:
- Use MongoDB with Node.js and Mongoose
- Design efficient schemas
- Create indexes for better performance
- Use aggregation for data analysis
- Follow best practices for production apps
Now you're ready to build real applications with MongoDB! Practice by building projects like a blog, todo app, or e-commerce store.