Chapter 3: Databases (MongoDB Basics)
Our Express server can now send "Hello World" and even basic JSON data. But there's a problem: every time we stop and restart the server, all our data is lost! We need a way to store information **persistently**. This is where databases come in.
A **database** is an organized collection of data, stored electronically. Think of it as a super-smart filing cabinet for your application's information. Your backend server (Node.js/Express) will talk to the database to save new data (Create), retrieve existing data (Read), update it (Update), or delete it (Delete).
Theory: SQL vs. NoSQL Databases
Databases generally fall into two main categories:
1. SQL (Relational Databases) - The Organized Spreadsheet
- Stores data in **tables** with predefined **columns** and **rows**.
- Excels at defining **relationships** between tables.
- Uses **SQL** to interact with data.
- Examples: PostgreSQL, MySQL.
- Pros: Data integrity, powerful querying for related data.
- Cons: Less flexible schema, horizontal scaling can be harder.
2. NoSQL (Non-Relational Databases) - The Flexible Filing Cabinet
- Offers more flexibility in how data is stored.
- Key Concept: **Flexibility & Scalability**.
- Types: Document (MongoDB), Key-Value (Redis), Column-Family (Cassandra), Graph (Neo4j).
- Pros: Flexible schema, easier horizontal scaling, often fast.
- Cons: Less strict relationship enforcement, consistency needs care.
Theory: Introducing MongoDB & Mongoose 🍃
MongoDB Core Concepts
MongoDB's structure:
- **Database:** Top-level container.
- **Collection:** Group of related documents (like a flexible table).
- **Document:** A single record in BSON format (like a JS object with a unique `_id`).
Mongoose: Making MongoDB Easier in Node.js
**Mongoose** is an **ODM (Object Data Modeling)** library. It provides a structured way to interact with MongoDB from Node.js.
It helps with:
- **Schema Definition:** Defining a blueprint (structure, types, validation) for your documents.
- **Models:** Creating tools from schemas to perform CRUD operations on collections.
- **Data Validation:** Enforcing rules before saving.
- **Query Building:** Cleaner syntax for database queries.
Task: Setting Up MongoDB & Connecting Securely
Let's get a database and connect our Express app without exposing secrets.
Step 1: Get a MongoDB Database Instance
Use the free tier on **MongoDB Atlas** (MongoDB's cloud service).
How to Perform (MongoDB Atlas Setup):
- Sign up at MongoDB Atlas.
- Create a **Free Tier Cluster**.
- Configure Security: Create a **Database User** (note the username/password) and **Whitelist your IP** (`0.0.0.0/0` for development only).
- Get Connection String: Click "Connect" -> "Connect your application". Copy the string. It will look like `mongodb+srv://
: @yourclustername.mongodb.net/YOUR_DB_NAME?retryWrites=true&w=majority`. - **Crucially:** Replace `
`, ` `, and `YOUR_DB_NAME` with your actual credentials and a database name. **Keep this complete string safe and private! DO NOT put it directly in your code.**
Step 2: Install Mongoose & Dotenv
Install Mongoose and `dotenv` (a package to manage environment variables).
How to Perform (Terminal):
In your project directory (e.g., `my-server`) run:
npm install mongoose dotenv
Step 3: Store Connection String Securely (Using `.env`)
Never hardcode secrets like your database connection string directly in your `server.js` file, especially if your code is on GitHub.
How to Perform:
- **Create `.env` file:** In the root of your project folder (next to `package.json`), create a new file named exactly `.env`.
- **Add URI to `.env`:** Inside the `.env` file, add your connection string like this (no quotes needed):
- **Create `.gitignore` file:** If you don't already have one, create a file named `.gitignore` in the root of your project.
- **Add `.env` to `.gitignore`:** Inside the `.gitignore` file, add a new line:
# Inside .env file
MONGO_URI=mongodb+srv://YOUR_USERNAME:YOUR_PASSWORD@yourcluster.mongodb.net/YOUR_DB_NAME?retryWrites=true&w=majority
# Inside .gitignore file
.env
node_modules/
This tells Git to **ignore** the `.env` file, so your secrets are never committed or pushed to GitHub.
Step 4: Connect in Your `server.js` (Securely)
Modify `server.js` to load the connection string from the environment variable defined in `.env`.
How to Perform (Modify `server.js`):
require('dotenv').config(); // 1. Load environment variables from .env file AT THE VERY TOP
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const PORT = process.env.PORT || 3000;
// --- Database Connection ---
// 2. Access the connection string from process.env
const MONGO_URI = process.env.MONGO_URI;
// Simple check if the URI loaded
if (!MONGO_URI) {
console.error('FATAL ERROR: MONGO_URI is not defined in .env file');
process.exit(1); // Exit if the connection string is missing
}
async function connectDB() {
try {
// 3. Use the loaded URI to connect
await mongoose.connect(MONGO_URI);
console.log('MongoDB Connected Successfully! ✅');
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
} catch (error) {
console.error('MongoDB Connection Error: ❌', error);
process.exit(1);
}
}
connectDB(); // Call the function
// --- Basic Route (Example) ---
app.get('/', (req, res) => {
const dbStatus = mongoose.connection.readyState === 1 ? 'Connected' : 'Disconnected';
res.send(`Hello World! DB Status: ${dbStatus}`);
});
// --- Other routes and middleware will go here later ---
Theory: Understanding the Secure Connection
- **`require('dotenv').config();`:** This line **must be at the very top** of your file. It loads the variables defined in your `.env` file and makes them available on the global `process.env` object in Node.js.
- **`process.env.MONGO_URI`:** Node.js automatically provides the `process.env` object. After running `dotenv.config()`, `process.env` will contain all the variables from your `.env` file. We access our connection string using the key we defined (`MONGO_URI`).
- **Security:** Your secret connection string now lives *only* in the `.env` file on your local machine (and needs to be set up separately on your deployment server). It's never committed to Git, keeping your credentials safe.
How to Perform (Testing the Connection):
- Ensure you have created the `.env` file with the correct `MONGO_URI`.
- Ensure you have added `.env` to your `.gitignore` file.
- Save `server.js`, `.env`, and `.gitignore`.
- Run `node server.js` in your terminal.
- You should see the `MongoDB Connected Successfully! ✅` message. If you see the "FATAL ERROR" message, double-check your `.env` file name and the variable name `MONGO_URI`. If you see a connection error, check the string details and Atlas IP whitelist.
Now your connection setup is secure!
Task: Defining Data Structure with Schemas & Models
(This section remains the same as before, defining the schema and model)
Schema: The Blueprint 📜
How to Perform (Add after `connectDB()` call):
// --- Define Schema & Model ---
const { Schema } = mongoose;
const userSchema = new Schema({
username: { type: String, required: true, unique: true, trim: true, minlength: 3 },
email: { type: String, required: true, unique: true, trim: true, lowercase: true, match: [/.+\@.+\..+/, 'Invalid Email'] },
age: { type: Number, min: 13, max: 120 },
isVerified: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now, immutable: true },
updatedAt: { type: Date, default: Date.now }
});
userSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
Model: The Working Tool 🛠️
How to Perform (Add after schema definition):
// Create the 'User' model
const User = mongoose.model('User', userSchema);
Task: Performing CRUD Operations with Mongoose
(This section also remains the same, defining the CRUD routes)
Middleware Setup: `express.json()`
How to Perform (Add near the top):
// --- Middleware ---
app.use(express.json());
1. Create (POST /users)
How to Perform (Add route definition):
// --- CRUD Routes for Users ---
app.post('/users', async (req, res) => {
try {
const newUser = new User(req.body);
const savedUser = await newUser.save();
res.status(201).json(savedUser);
} catch (error) {
res.status(400).json({ message: "Error creating user", details: error.message });
}
});
2. Read (GET /users, GET /users/:id)
How to Perform (Add route definitions):
// READ all users
app.get('/users', async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch (error) {
res.status(500).json({ message: "Error fetching users", details: error.message });
}
});
// READ one user by ID
app.get('/users/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ message: 'User not found' });
res.json(user);
} catch (error) {
res.status(500).json({ message: "Error fetching user", details: error.message });
}
});
3. Update (PATCH /users/:id)
How to Perform (Add route definition):
// UPDATE a user by ID
app.patch('/users/:id', async (req, res) => {
try {
const updatedUser = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!updatedUser) return res.status(404).json({ message: 'User not found' });
res.json(updatedUser);
} catch (error) {
res.status(400).json({ message: "Error updating user", details: error.message });
}
});
4. Delete (DELETE /users/:id)
How to Perform (Add route definition):
// DELETE a user by ID
app.delete('/users/:id', async (req, res) => {
try {
const deletedUser = await User.findByIdAndDelete(req.params.id);
if (!deletedUser) return res.status(404).json({ message: 'User not found' });
res.json({ message: 'User deleted successfully' });
} catch (error) {
res.status(500).json({ message: "Error deleting user", details: error.message });
}
});
Conclusion: Persistent & Secure Data Power! 💪
You've now learned how to securely connect your Node.js/Express backend to MongoDB using environment variables and Mongoose. You understand Schemas, Models, and CRUD operations. Your backend can now safely manage persistent data, a cornerstone of web development!