MVC Folder structure in MERN Stack
Hello friends,
My name is Sachin Mahor, and today I want to introduce you to the MVC folder structure. Many developers start projects without a proper folder structure, and as the project grows, they struggle to maintain the codebase. In this article, I’ll explain how using the MVC folder structure can help you organize your code better, making it easier to manage and scale your projects.
Introduction
Building a solid folder structure for your MERN project is crucial. The MERN stack — MongoDB, Express.js, React.js, and Node.js — is a powerful toolkit for building full-stack web applications. By following the MVC (Model, View, Controller) pattern, you can make your project more organized, scalable, and easier to maintain. In this article, I’ll guide you through setting up an MVC folder structure for your MERN project.
Get Sachin Mahor’s stories in your inbox
Join Medium for free to get updates from this writer.
brief summary of the importance of MVC architecture
(i) Model:
- Role: It is responsible for managing data of the application and define the structure of the data and also interacting with the database. So we define the structure of entities.
- Responsibility:
- Data Management: It handles data retrieval, update, deletion, and storage. It changes the data in the database.
- Data Validation: While data come from view(UI), It checks data integrity and validation. It does not allow invalid data in the database.
(ii) View:
- Role: It is responsible for managing the user interface(UI). It takes the data from the user and stores it in the database through the model and renders (Display to the user) data provided by the model.
- Responsibility:
- Rendering: The view renders the HTML, CSS, and client-side Javascript with the necessary data.
- User Interaction: The view captures the user input(such as clicks and form submission) and sends it to the controller for processing and storing in the database.
(iii) Controller:
- Role: It works as a bridge between view and model. It takes user data from the view processes the data and sends it to the model for storing the data in the database.
- Responsibility:
- Input Handling: The controller receives data from the view, processes them, and then takes appropriate action.
- Business logic: The controller contains the business logic of the application, It takes particular business logic based on route.
- Updating View and Model: It is used to determine which action to take, the controller updates the model and it will reflect on the view.
Setting up MVC folder structure
A well-organized folder structure ensures that your application is clear and maintainable as it grows. I suggest you follow the MVC folder structure.
mern-app/
│
├── backend/
│ ├── config/
| | ├── cloudinary.js
│ │ └── database.js
│ ├── controllers/
│ │ ├── Auth.js
| | ├── Complaint.js
│ │ └── Contact.js
│ ├── mail/templates
│ │ ├── complaintAccepted.js
| | ├── complaintRejected.js
| | ├── complaintSolved.js
| | ├── contactUsMail.js
| | ├── emailVerificationTemplate.js
│ │ └── complaintSubmission.js
│ ├── middleware/
│ │ └── auth.js
│ ├── models/
│ │ ├── Complaint.js
│ │ ├── ContactUs.js
│ │ ├── OTP.js
│ │ ├── ServiceProvider.js
│ │ ├── Student.js
│ │ └── User.js
│ ├── node_modules/
│ ├── routes/
│ │ ├── Complaint.js
│ │ ├── User.js
│ │ └── ContactUs.js
│ ├── utils/
│ │ ├── mailSender.js
│ │ └── imageUploader.js
│ ├── server.js
│ └── .env
│
├── frontend/
│ ├── public/
│ │ └── index.html
│ ├── src/
│ │ ├── assets/
│ │ │ ├── data
│ │ │ ├── image
│ │ ├── components/
│ │ │ ├── Homepage
│ │ │ ├── AddComplaint.jsx
│ │ │ ├── AddNewUser.jsx
│ │ │ ├── AnalyticCard.jsx
│ │ │ ├── ContactUs.jsx
│ │ │ ├── DashBoard.jsx
│ │ │ └── EditProfile.js
│ │ ├── pages/
│ │ │ └── HomePage.js
│ │ ├── App.js
│ │ ├── index.js
│ │ └── index.css/
│ └── package.json
│
├── package.json
├── README.md
└── .gitignoreBackend Structure
The backend folder structure is responsible for handling server-side logic, processing the data and the store in the database.
- Config: This folder contains the configure files. for example, ‘database.js’ is used to set up the database connection, and ‘Cloudinary’ is used to set up the connection to the cloud database for images and video.
const mongoose = require("mongoose");
const colors = require("colors")
require("dotenv").config();
exports.connect = () => {
mongoose.connect(process.env.MONGO_URL, {
})
.then(() => console.log(colors.green('Database Connected Successfully')))
.catch((error) => {
console.log(colors.red.underline('Database Connection Filed'))
confirm.error(error);
process.exit(1);
})
};2. Controller: The controller is responsible for handling request and response logic. Whenever the user requests from the front end then the request is solved and sent to respond back to the user.
const Complaint = require("../models/Complaint");
const User = require("../models/User");
const { uploadImageToCloudinary } = require("../utils/imageUploader");
// Create complaint
exports.createComplaint = async (req, res) => {
try {
const { complaintType, complaint } = req.body;
let image = ""; // Initialize image variable
// Check if complaintImage exists in req.files
if (
req.files &&
req.files &&
req.files["complaintImage[fileList][0][originFileObj]"]
) {
const complaintImage =
req.files["complaintImage[fileList][0][originFileObj]"];
console.log("Complaint image", complaintImage);
// Upload image to Cloudinary
image = await uploadImageToCloudinary(
complaintImage,
process.env.FOLDER_NAME,
1000,
1000
);
console.log("Upload image details", image);
}
// Fetch student details
const studentDetails = await User.findOne({ _id: req.user.id });
console.log("Student details: ", studentDetails);
// Check if there's any pending complaint of the same type
const pendingComplaint = await Complaint.find({
complaintType,
sid: studentDetails._id,
});
if (pendingComplaint.length == 3) {
return res.status(400).json({
success: false,
message: "Student Can not submit more than 3 complaints of same type",
});
}
// Create complaint
const response = await Complaint.create({
complaintType,
complaint,
complaintImage: image ? image.secure_url : "",
sid: studentDetails._id,
});
// Populate complaint details with student information
const complaintDetails = await Complaint.findById(response._id).populate(
"sid"
);
// Remove password from student details
complaintDetails.sid.password = undefined;
// Return response
return res.status(200).json({
success: true,
message: "Complaint submitted successfully",
complaintDetails,
});
} catch (error) {
console.log("Error occurred while submitting", error);
return res.status(500).json({
success: false,
error: error.message,
});
}
};3. Mail: Mail contains the template for a particular action. when the action is performed, the particular mail will be sent. We define actions in the model using pre/post middleware.
exports.complaintAccepted = (name, complaintId) => {
return `<!DOCTYPE html>
<html>
<head>
<title>Hello World!</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="container">
<h1>Complaint Reviewed and Forwarding Confirmation</h1>
<p>Dear ${name},</p>
<p>We hope this message finds you well.</p>
<p>Your Complaint Number: ${complaintId}</p>
<p>We want to inform you that your complaint has been successfully reviewed by our administrative team. </p>
<p>We appreciate your patience throughout this process.</p>
<p>After careful consideration, your complaint has been forwarded for further processing.</p>
<p> Rest assured, we are actively working towards addressing your concerns and finding a suitable resolution.</p>
<p>We will keep you updated on the progress of your complaint and notify you once a resolution has been reached.
</p>
<p>Thank you for bringing this matter to our attention and for your cooperation.</p>
<p>Sencerely,<br />Campus Voice</p>
</div>
</body>
</html>`
}4. Middleware: Middleware functions process requests before they reach the controller. For example, if a user wants to log in to the system, middleware checks whether the user is authenticated. If the user is authenticated, the request moves to the controller; otherwise, the middleware returns a response with an error.
const JWT = require("jsonwebtoken");
require("dotenv").config();
exports.auth = async(req, res, next) => {
try {
const token = req.body.token ||
req.cookies.token ||
(req.header("Authorization") || "").replace("Bearer", "").trim();
// if token is missing
if(!token){
return res.status(401).json({
success: false,
message: "Token is missing"
})
}
// verify the token
try {
const decode = JWT.verify(token, process.env.JWT_SECRET);
console.log("token",decode)
req.user = decode;
} catch (error) {
// verification issue
return res.status(401).json({
success: false,
message: "Token is invalid"
})
}
next()
} catch (error) {
return res.status(401).json({
success: false,
message: "Something went wrong while verifing token"
})
}
}5. Models: A model is used to define a schema for the database. For example: the user schema contains name and email. And also we create different- different schema of the application.
const mongoose = require("mongoose");
const mailSender = require("../utils/mailSender");
const { complaintSubmission } = require("../mail/templates/complaintSubmission");
const User = require("../models/User");
const { complaintAccepted } = require("../mail/templates/complaintAccepted");
const { complaintRejected } = require("../mail/templates/complaintRejected");
const { complaintSolved } = require("../mail/templates/complaintSolved");
const complaintSchema = new mongoose.Schema({
complaintType:{
type: String,
enum: ["Maintenance", "Safety and Security", "Food"],
required: true
},
complaint:{
type: String,
required: true
},
complaintImage:{
type: String,
},
status:{
type: String,
enum: ["Pending","Processing", "Solved", "Rejected"],
default: "Pending",
},
sid:{
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "User"
},
feedback:{
type: String,
},
createdAt:{
type: Date,
required: true,
default: Date.now,
},
updatedAt:{
type: Date,
required: true,
default: Date.now,
},
})
async function createComplaint(name,email,complaintId){
try {
console.log("Enter in email section");
const mailResponse = await mailSender(
email,
"Complaint Submitted Successfully",
complaintSubmission(name,complaintId)
)
} catch (error) {
console.log("Error occurred while creating complaint", error)
throw error
}
}
complaintSchema.post("save", async function(doc){
console.log("doc:", doc); // Moved inside the middleware function
try {
const userDetails = await User.findById(doc.sid);
if(userDetails){
await createComplaint(userDetails.name,userDetails.email, doc._id)
}
else{
console.log("User not found");
}
} catch (error) {
alert("Network issue, Please try after sometimes");
}
});
module.exports = mongoose.model("Complaint", complaintSchema);6. Routes: Routes are used to define the endpoints for the API. They serve as a way to direct API requests to the appropriate destination. Just as we check a route to reach a physical destination, routes in an API guide request to the correct endpoints.
const express = require("express");
const router = express.Router();
const { createComplaint, updateComplaint, deleteComplaint, getStudentComplaint, changeComplaintStatusByAdmin, getAdminComplaints, changeComplaintStatusByServiceProvider, getServiceProviderComplaints, getAllComplaints } = require("../controllers/Complaint");
const { auth, isStudent, isAdmin, isServiceProvider } = require("../middleware/auth")
router.post("/create",auth, isStudent, createComplaint);
router.post("/update",auth, isStudent, updateComplaint);
router.post("/delete",auth, isStudent, deleteComplaint);
router.get("/studentComplaints",auth, isStudent, getStudentComplaint);
router.post("/changeStatusByAdmin",auth, isAdmin, changeComplaintStatusByAdmin);
router.get("/adminComplaints",auth, isAdmin, getAdminComplaints);
router.post("/changeStatusByServiceProvider",auth, isServiceProvider, changeComplaintStatusByServiceProvider);
router.get("/serviceProviderComplaints",auth, isServiceProvider, getServiceProviderComplaints);
router.get("/getAllComplaints", getAllComplaints);
module.exports = router;7. Utils: Utils contain reusable code like mailSender.js: a function for sending mail.
const nodemailer = require("nodemailer");
require("dotenv").config();
const mailSender = async (email, title, body) => {
try {
let transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST,
auth:{
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS
}
})
let info = await transporter.sendMail({
from: "NIT Jamshedpur",
to: `${email}`,
subject: `${title}`,
html: `${body}`
})
console.log("info", info);
return info
} catch (error) {
console.log("Error occurred while sending a mail");
console.log(error)
}
}
module.exports = mailSender;8. Dotenv: It contains sensitive data like MongoDB URL, email password, keys, etc.
PORT = 4000
MONGO_URL = mongodb://127.0.0.1:27017/database_name
JWT_SECRET = "sachin"
# Nodemailer
MAIL_HOST = smtp.gmail.com
MAIL_USER =
MAIL_PASS =
# Cloudinary
FOLDER_NAME = 'campusVoice'
CLOUD_NAME =
API_KEY =
API_SECRET = 9. Server: The entry point for the backend. set up the server and listen to the request of the user. And middleware and routes are mounted.
const express = require("express");
const app = express();
// Import Routes
const userRoutes = require('./routes/User');
const complaintRoutes = require('./routes/Complaint');
const contactusRouter = require("./routes/ContactUs")
const database = require("./config/database");
const cookieParser = require("cookie-parser");
const bodyParser = require("body-parser")
const cors = require("cors");
const {cloudinaryConnect} = require("./config/cloudinary");
const fileUpload = require("express-fileupload");
const dotenv = require("dotenv");
dotenv.config();
const PORT = process.env.PORT;
// Database connectivity
database.connect();
// Middleware
app.use(express.json());
app.use(cookieParser());
app.use(bodyParser.json())
app.use(cors({
origin: "https://campusvoice.vercel.app",
credentials: true
}))
app.use(
fileUpload({
useTempFiles: true,
tempFileDir: "/tmp/"
})
)
// cloudinary connectivity
cloudinaryConnect();
// Route mount
app.use("/api/v1/auth", userRoutes);
app.use("/api/v1/complaint", complaintRoutes);
app.use("/api/v1/contact", contactusRouter)
// default route
app.get("/",(req, res) => {
return res.json({
success: true,
message: "Home Page"
})
})
app.listen(PORT, (error) => {
console.log(`Server is running on port ${PORT}`)
})Frontend Structure
The frontend folder contains all the React.js code. The react folder structure contains JSX(Javascript XML) code. JSX allows us to write HTML code in React.
- Public: This contains the static file that can not be changed, such as the HTML file where your react app mounts.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MERN App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>2. Src: It contains the source code of the react.js project.
- Assets: It contains static data like images, fonts, files, etc.
- Components: Reusable UI functions. It is a building block of react application. It is used within pages.
import React from "react";
import AnalyticCard from "./AnalyticCard";
const DashBoard = ({allComplaints}) => {
console.log("All Complaints:", allComplaints)
const noComplaint = allComplaints.length;
const pending = allComplaints.filter(complaint => complaint.status === "Pending")
const processing = allComplaints.filter(complaint => complaint.status === "Processing")
const rejected = allComplaints.filter(complaint => complaint.status === "Rejected")
const solved = allComplaints.filter(complaint => complaint.status === "Solved")
console.log("Pending Complaints", pending)
return (
<div>
<div className="flex flex-col gap-3 md:flex-row md:flex-wrap md:justify-center lg:gap-16">
<AnalyticCard title={"Pending"} count={pending.length} percentage={`${((pending.length/noComplaint)*100).toFixed(2)}%`} color={"#b8fef9"}/>
<AnalyticCard title={"Processing"} count={processing.length} percentage={`${((processing.length/noComplaint)*100).toFixed(2)}%`}/>
<AnalyticCard title={"Solved"} count={solved.length} percentage={`${((solved.length/noComplaint)*100).toFixed(2)}%`}/>
<AnalyticCard title={"Rejected"} count={rejected.length} percentage={`${((rejected.length/noComplaint)*100).toFixed(2)}%`}/>
</div>
</div>
);
};
export default DashBoard;- pages: Pages are standalone views. pages contain different-different routes on one page like the homepage contains more than one route. And also use components within the page.
import React from 'react';
const HomePage = () => {
return (
<div>
<h1>Home Page</h1>
</div>
);
};
export default HomePage;- app.js: The main application component that serves as the root of the component tree and contains the core application logic. This component is written in an index.js file.
import './App.css';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Homepage from './pages/Homepage';
import Navbar from './components/Navbar';
import SendOTP from './components/SendOTP';
import SignUp from './components/SignUp';
import Login from './components/Login';
import ShowComplaint from './components/ShowComplaint';
import AddComplaint from "./components/AddComplaint";
import ContactUs from "./components/ContactUs"
import { useState } from 'react';
import ViewProfile from './components/ViewProfile';
import AddNewUser from "./components/AddNewUser"
function App() {
const [token, setToken] = useState("")
const [profileDetails, setProfileDetails] = useState({})
const [complaintData, setComplaintData] = useState()
return (
<Router>
<Navbar setToken={setToken} setProfileDetails={setProfileDetails} />
<Routes>
<Route path='/' element={<Homepage token={token} profileDetails={profileDetails} complaintData={complaintData}/>}></Route>
<Route path='/sendotp' element={<SendOTP />}></Route>
<Route path='/signup' element={<SignUp />}></Route>
<Route path='/login' element={<Login />}></Route>
<Route path='/showcomplaints' element={<ShowComplaint setComplaintData={setComplaintData} />}></Route>
<Route path='/addcomplaint' element={<AddComplaint />}></Route>
<Route path='/contactus' element={<ContactUs />}></Route>
<Route path='/viewprofile' element={<ViewProfile profileDetails={profileDetails} token={token}/>}></Route>
<Route path='/addnewuser' element={<AddNewUser token={token}/>}></Route>
</Routes>
</Router>
);
}
export default App;- index.js: The entry point for the React application, responsible for rendering the root component into the DOM.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css'; // Optional, for global styles
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);Conclusion:
MVC architecture helps in the MERN stack to manage a clean codebase. It is useful to debug code easily when the size of the project grows. Developers can easily collaborate with this structure.





