Day 05 - MERN: Mongoose Models, Routers, and Project Structure
Welcome to Day 5! As our backend applications grow, putting all our code into a single file quickly becomes unmanageable. Today, we will organize our project structure and learn how Express communicates with MongoDB using schemas, models, and modular routers.
1. Mongoose: Schemas and Models
While we can talk to MongoDB directly, most developers use Mongoose, an Object Data Modeling (ODM) library. It creates a bridge between your JavaScript objects and your MongoDB documents, saving you from writing complicated database queries.
To use Mongoose, you need two things:
- Schema (The Blueprint): Defines the exact structure of your documents (the fields, data types, and rules).
- Model (The Interface): Wraps the schema and provides the actual commands (like
.find()or.save()) to interact with the database collection.
import mongoose from "mongoose";
// 1. The Schema (Blueprint)
const studentSchema = new mongoose.Schema({
name: String,
age: Number,
city: String
});
// 2. The Model (Database Interface)
// Mongoose automatically converts "Student" to the plural "students" collection
const Student = mongoose.model("Student", studentSchema);
// Export it so other files can use it
export default Student;
2. Saving Data and Handling Promises
Database operations take time. Because Node.js does not want to freeze your server while waiting for the database, these operations return a Promise—a placeholder for a future result that will eventually succeed or fail.
Creating a new document using your model does not instantly save it to MongoDB. You must explicitly call .save(). We handle this asynchronous process using async/await.
app.post("/", async (req, res) => {
try {
// Create a JS object from the incoming client data
const newStudent = new Student(req.body);
// Wait for the database to actually store the document
await newStudent.save();
res.json({ message: "Successfully saved to database" });
} catch (error) {
// Catch and respond to any database errors
res.status(500).json({ message: "Failed to save", error: error.message });
}
});
3. Express Router and Clean Architecture
Writing every single API inside index.js is a bad practice. Instead, we follow the Single Responsibility Principle by separating our code into folders: a models folder for database structures, and a routers folder for API endpoints.
express.Router() creates a mini Express application. You group related routes (like everything related to students) in one file, and plug that router into your main application.
// routers/studentRouter.js
import express from "express";
import Student from "../models/student.js";
const studentRouter = express.Router();
studentRouter.get("/", async (req, res) => {
const students = await Student.find(); // Retrieves all students
res.json(students);
});
export default studentRouter;
// index.js (Main Application)
import express from "express";
import studentRouter from "./routers/studentRouter.js";
const app = express();
// Automatically prefixes all routes in the router with "/students"
app.use("/students", studentRouter);
4. The Magic of ObjectId
When you successfully save a document, MongoDB automatically generates a unique identifier for it called _id.
- 12-Byte Format: This is not a standard UUID. It is a specific 12-byte
ObjectIdgenerated by MongoDB. - Built-in Timestamp: The first part of the
ObjectIdcontains a timestamp of exactly when the document was created, meaning documents naturally sort chronologically without needing a separate “created at” date field. - Primary Key: You will use this
_idconstantly in the future to find, update, or delete specific documents (e.g.,Student.findById(id)).
Comments
Questions, corrections, and practical takeaways are welcome here.