MongoDB is a leading NoSQL document database. Instead of rows and columns, data is stored as flexible JSON-like documents. Mongoose is an Object Data Modeling (ODM) library for Node.js that provides a schema-based solution to model your application data.
Step 1 — Setting up the Connection
Install Mongoose via npm (npm install mongoose). Then, connect your Express server to a local or cloud (MongoDB Atlas) database instance.
const mongoose = require('mongoose');
require('dotenv').config();
const connectDB = async () => {
try {
// Connect using connection string from environment variables
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('✅ MongoDB connected successfully');
} catch (error) {
console.error('❌ MongoDB connection failed:', error.message);
process.exit(1); // Exit process with failure
}
};
module.exports = connectDB;Step 2 — Defining a Mongoose Schema
While MongoDB is schema-less, Mongoose enforces structure at the application level. Schemas define the shape, types, and validation rules for documents.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true
},
email: {
type: String,
required: true,
unique: true, // Creates an index for uniqueness
lowercase: true
},
age: {
type: Number,
min: [18, 'Must be an adult'],
default: 18
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
}, {
timestamps: true // Automatically adds createdAt and updatedAt fields
});
// Create the Model based on the Schema
const User = mongoose.model('User', userSchema);
module.exports = User;Step 3 — CRUD Operations: Create & Read
Mongoose models provide rich async functions to interact with the database.
const User = require('../models/User');
// CREATE (POST)
exports.createUser = async (req, res) => {
try {
// This validates against the schema
const newUser = await User.create(req.body);
res.status(201).json(newUser);
} catch (err) {
res.status(400).json({ error: err.message });
}
};
// READ (GET)
exports.getAllUsers = async (req, res) => {
try {
// Find allows querying, filtering, and sorting
const users = await User.find({ role: 'user' }).sort('-createdAt').limit(10);
res.json(users);
} catch (err) {
res.status(500).json({ error: err.message });
}
};
exports.getUserById = async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ message: 'Not found' });
res.json(user);
};Step 4 — CRUD Operations: Update & Delete
// UPDATE (PATCH/PUT)
exports.updateUser = async (req, res) => {
try {
// findByIdAndUpdate(id, dataToUpdate, options)
// new: true returns the updated document, runValidators ensures schema rules run on updates
const updatedUser = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!updatedUser) return res.status(404).json({ message: 'Not found' });
res.json(updatedUser);
} catch (err) {
res.status(400).json({ error: err.message });
}
};
// DELETE (DELETE)
exports.deleteUser = async (req, res) => {
try {
const deleted = await User.findByIdAndDelete(req.params.id);
if (!deleted) return res.status(404).json({ message: 'Not found' });
res.status(204).send(); // 204 No Content
} catch (err) {
res.status(500).json({ error: err.message });
}
};Step 5 — Relations: Embedding vs Referencing
Because NoSQL lacks SQL-style JOINs, you have two primary ways to handle relationships: Embedding (nesting objects) or Referencing (linking via ObjectIds).
When to use what?
- Embedding (1-to-Few): Store an array of addresses directly inside the User document. Faster reads, but bad for unbounded growth.
- Referencing (1-to-Many): Store a user ObjectId inside a Post document. Use Mongoose
populate()to join them when reading. Best for large data.
// Post schema references the User model
const postSchema = new mongoose.Schema({
title: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
});
// Fetching a post and eager-loading the author's name and email
const post = await Post.findById(postId).populate('author', 'name email');
console.log(post.author.name); // Automatically joined!