Skip to main content
Express.js Tutorial
CHAPTER 11 Beginner

Connecting Express.js with MongoDB

Updated: May 14, 2026
35 min read

# CHAPTER 11

Connecting Express.js with MongoDB

1. Introduction

A backend application without a database is like a computer without a hard drive; it forgets everything the moment you turn it off. To build real applications, you must persist data. MongoDB is the most popular database choice for Node.js developers because it stores data in JSON-like documents, seamlessly matching JavaScript's native object structure. In this chapter, we will learn how to connect Express to MongoDB and execute CRUD queries using an Object Data Modeler (ODM) called Mongoose.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Establish a connection to a MongoDB database.
  • Define data schemas and compile them into Models using Mongoose.
  • Perform asynchronous CRUD operations on a real database.
  • Understand the benefits of using an ODM over raw queries.

3. Beginner-Friendly Explanation

Imagine a massive warehouse (MongoDB). You *could* drive a forklift in there yourself, frantically searching for boxes, and trying to remember where you put everything (Raw Database Queries). Instead, you hire a highly organized warehouse manager named Mongoose. Mongoose creates blueprints (Schemas) for exactly what a "Box" should look like, and prevents anyone from putting a shoe into a box meant for hats. When you need data, you simply ask Mongoose, "Bring me the box with ID #5," and Mongoose retrieves it for you.

4. Step 1: Installing Mongoose

Writing raw MongoDB queries in Node.js is tedious. We use the Mongoose NPM package to act as our warehouse manager.
bash
1
npm install mongoose

5. Step 2: Establishing the Connection

In a professional application, you connect to the database *before* turning on the server. If the database is offline, the server shouldn't start.

In index.js:

javascript
12345678910111213141516171819
const express = require('express');
const mongoose = require('mongoose');
const app = express();

app.use(express.json()); // Essential for POST requests

// The Connection String (Localhost example)
const DB_URI = 'mongodb://localhost:27017/my_express_db';

// Connect to MongoDB asynchronously
mongoose.connect(DB_URI)
    .then(() => {
        console.log("MongoDB Connected Successfully!");
        // Only start listening AFTER the database is connected
        app.listen(3000, () => console.log("Server running on port 3000"));
    })
    .catch((err) => {
        console.error("Database connection failed:", err);
    });

6. Step 3: Designing a Schema

MongoDB is schema-less. It allows you to put completely random data into the same collection. This is dangerous for a predictable API. We use Mongoose to enforce strict blueprints.

Create a file: models/User.js

javascript
1234567891011
const mongoose = require('mongoose');

// Define the Blueprint
const userSchema = new mongoose.Schema({
    name: { type: String, required: true },
    email: { type: String, required: true, unique: true },
    age: { type: Number, default: 18 }
}, { timestamps: true }); // Automatically adds createdAt and updatedAt fields

// Compile the schema into a Model and export it
module.exports = mongoose.model('User', userSchema);

7. Step 4: Executing Database Queries (CRUD)

Now we import our Model into our Controller and use modern async/await syntax to interact with the database.

In controllers/userController.js:

javascript
1234567891011121314151617181920212223242526272829303132333435363738394041
const User = require('../models/User');

// CREATE
const createUser = async (req, res) => {
    try {
        // Create a new instance of the User model using the request body
        const newUser = new User(req.body);
        
        // Save it to MongoDB
        await newUser.save();
        
        res.status(201).json({ status: "success", data: newUser });
    } catch (err) {
        res.status(400).json({ error: err.message });
    }
};

// READ ALL
const getUsers = async (req, res) => {
    try {
        // Find ALL users in the database
        const users = await User.find();
        res.status(200).json({ status: "success", data: users });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
};

// READ ONE
const getUserById = async (req, res) => {
    try {
        const user = await User.findById(req.params.id);
        if (!user) return res.status(404).json({ error: "User not found" });
        
        res.status(200).json({ status: "success", data: user });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
};

module.exports = { createUser, getUsers, getUserById };

8. Backend Workflow: Handling Promises

Database operations take time (a few milliseconds to go over the network and read the hard drive). Therefore, Mongoose methods like .find() and .save() return Promises. You must ALWAYS prefix these commands with await, and the surrounding function MUST be marked as async. If you forget await, Express will send the response before the database has finished searching, resulting in a blank or pending object.

9. Best Practices

  • Never Hardcode Credentials: In Step 2, we hardcoded mongodb://localhost.... If you are using MongoDB Atlas (the cloud version), that string contains your database password! Never type it directly into index.js. Always store the connection string in a hidden .env file.

10. Common Mistakes

  • Server Crashing on Invalid IDs: MongoDB uses a very specific 24-character string for its ObjectIDs (e.g., 5f8a...). If a client requests /api/users/123, Mongoose will crash your server because 123 is not a valid 24-character hex string. You must wrap your database calls in try/catch blocks (as shown in Step 7) so that if Mongoose throws a "CastError", your server catches it and returns a clean JSON error instead of completely shutting down.

11. Exercises

  1. 1. Look at the User schema in Step 3. What is the architectural benefit of adding the { timestamps: true } option to the Mongoose schema?

12. Coding Challenges

  • Challenge: Using Mongoose syntax, write the controller logic for a deleteUser function. It should use await User.findByIdAndDelete(req.params.id) and return a 200 OK JSON message upon success.

13. MCQs with Answers

Question 1

What is the primary purpose of Mongoose in an Express application?

Question 2

Why must database queries in an Express controller be wrapped inside async functions and utilize the await keyword?

14. Interview Questions

  • Q: Explain why Node.js and MongoDB are frequently paired together (the "M" and "N" in the MERN stack). What is the underlying data structure they share?
  • Q: What is a Schema in Mongoose, and why do we apply strict schemas to a database (MongoDB) that is natively "schema-less"?

15. FAQs

Q: Can I use MongoDB without Mongoose? A: Yes, you can install the official mongodb npm driver to write raw queries. However, raw queries lack schema validation. Mongoose is the industry standard for 95% of Node.js enterprise applications.

16. Summary

In Chapter 11, we gave our API a memory. We explored MongoDB and integrated the powerful Mongoose ODM to enforce strict architectural blueprints (Schemas) on our data. By utilizing asynchronous JavaScript (async/await), we successfully executed Create and Read operations against a live database, catching potential errors gracefully to ensure our server never crashes.

17. Next Chapter Recommendation

MongoDB is great for flexible data, but what if you work for a bank that requires strict relational tables? Proceed to Chapter 12: Connecting Express.js with MySQL.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·