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.

db.jsjavascript
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.

models/User.jsjavascript
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.

controllers/userController.jsjavascript
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

controllers/userController.jsjavascript
// 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.
populate.jsjavascript
// 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!