How to Follow the MVC Pattern in an Express Mongoose App
If you’re working with Node.js and MongoDB, Express and Mongoose are two popular tools for building robust applications. One of the most efficient and scalable ways to structure your Express-Mongoose app is by following the MVC pattern. In this article, we’ll explore how you can apply the MVC (Model-View-Controller) pattern in an Express app using Mongoose for database operations.

The MVC architecture helps organize your code by separating concerns into three distinct parts:
- Model: Manages data and database interaction.
- Controller: Handles logic and communication between models and views.
- View: (In this case, the API response) presents data to users, often in JSON format.
- Routes and Middleware: These act as connectors between the MVC components and enhance the flow of requests.
Let’s dive deeper into each of these components and how they interact with each other in a typical Express-Mongoose setup.
1. Setting Up Your Express Application
Before getting started, ensure you have a basic Express app setup with Mongoose connected to your MongoDB database.
npm init -y
npm install -g nodemon
npm install express mongoose dotenv bcrypt cors cookie-parser jsonwebtoken
2. Prepare your folder structure like this:
/myapp
│ .env
│ index.js
│
├───controllers
│ userController.js
├───db
│ connectDB.js
│
├───middlewares
│ authMiddleware.js
│
├───models
│ User.js
│
├───routes
│ userRoutes.js
3. Create a file named .env
Store all of your sensitive information here:
MONGO_URI = 'MONGO_STRING_URI'
PORT = 4732
ACCESS_TOKEN = 'ACCESS_TOKEN' //generate from node.js crypto
4. Database Connection (db/connectDB.js
)
The Model layer involves defining how the data interacts with MongoDB. Before setting up the models, we need to establish a connection to the database.
import mongoose from 'mongoose';
export const connectDB = async (url) => {
if (!url) {
console.log("Please provide a database URI string");
process.exit(1);
}
try {
await mongoose.connect(url);
console.log('Database connection established!');
} catch (error) {
console.error(`Database Connection Failed: ${error.message}`);
process.exit(1);
}
};
5. Model: Defining the Data Structure
In the MVC pattern, the Model represents the structure of the data in your application. In an Express-Mongoose app, models are defined using Mongoose schemas.
Create a folder named models
to store your Mongoose models.
import mongoose from "mongoose";
const Schema = mongoose.Schema;
const userSchema = new Schema({
username: {
type: String,
required: "Username is required!",
unique: true,
},
email: {
type: String,
required: "Email is required!",
unique: true,
},
password: {
type: String,
required: "Password is required",
},
role: {
type: String,
enum: ["admin", "manager", "cashier"],
required: "Role is required",
},
shop_id: {
type: Schema.Types.ObjectId,
required: "Shop ID is required",
ref: "Shop",
},
created_at: { type: Date, default: Date.now },
updated_at: { type: Date, default: Date.now },
});
export const User = mongoose.model('User', userSchema)
6. Writing Controllers (controllers/user.js
)
The Controller is responsible for handling the logic of the application. It receives input from the routes, interacts with the models, and sends the appropriate response.
Create a controllers
folder to store your controllers.
import bcrypt from "bcrypt";
import jwt from 'jsonwebtoken';
import { User } from "../models/user.js";
export const createUser = async (req, res) => {
try {
const user = req.body;
const hashedPassword = await bcrypt.hash(user.password, 13);
const newUser = new User({
...user,
password: hashedPassword,
});
const result = await newUser.save();
res.status(201).send({ success: true, message: "Successfully Registered!", data: result });
} catch (error) {
if (error.code === 11000) {
return res.status(400).send({ success: false, message: "Email Already Exists!" });
}
res.status(500).send({ success: false, message: error.message });
}
};
export const loginUser = async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) return res.status(400).send({ success: false, message: "User not found!" });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(400).send({ success: false, message: "Invalid credentials" });
const token = jwt.sign({ email }, process.env.ACCESS_TOKEN, { expiresIn: '1h' });
res.status(200).send({ success: true, token, data: user });
} catch (error) {
res.status(500).send({ success: false, message: error.message });
}
};
// Other controller functions for getAllUser, getUser, editUser, deleteUser...
6. Setting Up Routes (routes/user.js
)
The Routes are responsible for defining the paths your application exposes and mapping those paths to specific controller actions.
Create a routes
folder to organize your route files.
import express from "express";
import { createUser, loginUser, getAllUser, getUser, editUser, deleteUser } from "../controllers/user.js";
import { verifyToken } from "../middlewares/verifyToken.js";
const router = express.Router();
router.get("/", getAllUser);
router.post("/register", createUser);
router.post("/login", loginUser);
router.route("/me/:email")
.get(verifyToken, getUser)
.patch(verifyToken, editUser)
.delete(verifyToken, deleteUser);
export default router;
Here, we’ve created six routes:
POST /register
for creating a new user.POST /
for getting all user.POST /login
for login a new user.GET /me/:email
for get a user details.PATCH /me/:email
for edit a user details.DELETE /me/:email
for delete a user.
7. Using Middleware (middlewares/verifyToken.js
)
Middleware plays a crucial role in handling tasks like authentication, logging, or error handling. Here’s a basic token verification middleware:
import jwt from 'jsonwebtoken';
export const verifyToken = (req, res, next) => {
const token = req.headers['authorization'];
if (!token) return res.status(401).send({ success: false, message: "Access Denied" });
try {
const verified = jwt.verify(token, process.env.ACCESS_TOKEN);
req.user = verified;
next();
} catch (error) {
res.status(400).send({ success: false, message: "Invalid Token" });
}
};
- `verifyToken` checks if a valid JWT token is present in the request headers before proceeding to the protected route.
8. Putting It All Together
Now that we have set up all the core components (Model, Controller, Routes, Middleware) in your index.js
like this:
The index.js
file is the entry point of your application. It sets up the server, middlewares, and routes.
import cors from 'cors';
import dotenv from 'dotenv';
import express from 'express';
import { connectDB } from './db/connectDB.js';
import { logReqRes } from './middlewares/index.js';
import UserRouter from './routes/user.js'; // Importing user-related routes
dotenv.config(); // Load environment variables
const app = express();
const PORT = process.env.PORT || 3478;
// Middleware for JSON parsing and enabling CORS
app.use(express.json());
app.use(
cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
})
);
// Custom middleware for logging request and response
app.use(logReqRes("log.txt"));
// Register the user router
app.use("/auth/users", UserRouter);
// Health check endpoint
app.get('/', (req, res) => {
res.send({
status: 200,
success: true,
message: "Server Running...",
});
});
// Handle 404 errors
app.use((req, res, next) => {
const error = new Error("Requested URL Not Found");
error.status = 404;
next(error);
});
// Global error handling middleware
app.use((error, req, res, next) => {
console.log(error);
res.status(error.status || 500).send({
status: error.status || 500,
success: false,
message: error.message || "Internal Server Error"
});
});
// Database connection and server start
(async () => {
try {
await connectDB(process.env.MONGO_URI);
app.listen(PORT, () =>
console.log("APP RUNNING ON PORT:", PORT)
);
} catch (error) {
console.error("Failed to Start the Server: ", error);
process.exit(1);
}
})();
Congratulations! Your MVC pattern is complete, and you can now write code in a clean and organized way. Open your console (CTRL+J) and type nodemon index.js
. If you have set up everything correctly, you will see this in your console:
PS F:\backend\pos-server> nodemon index.js
[nodemon] 3.1.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node index.js`
Database connection established!
APP RUNNING ON PORT: 4732
Conclusion
Implementing the MVC pattern in your Express-Mongoose application ensures a well-organized and scalable architecture. By clearly separating Models, Controllers, and Routes, you create a maintainable codebase that is easier to manage and extend. This approach not only simplifies development but also enhances collaboration and debugging, setting a strong foundation for future growth. Embracing MVC helps you build a robust application with clean and efficient code.