In this article, we’ll walk through the step-by-step process of creating a Expense Management System using the MERN (MongoDB, ExpressJS, React, NodeJS) stack. This project will showcase how to set up a full-stack web application where users can add their budget and put daily expenses that get deducted from the budget.
Output Preview: Let us have a look at how the final output will look like.

Prerequisites:
Approach to Create Expense Management System using MERN:
- List all the requirement for the project and make the structure of the project accordingly.
- Chooses the required dependencies and requirement which are more suitable for the project.
For Backend:-
- Create a directory named model inside root directory.
- Create javascript files named User.js and Budget.js in the model directory for collection schema.
- Then create another route directory inside root(Backend folder).
- Create javascript files named auth.js and budget.js to handle API request.
For Frontend:-
- Create a components directory inside root directory( Budget_Tracker folder).
- Create four file of javascript inside components folder namely Expense.jsx, Home.jsx, RegistrationForm.jsx and LoginForm.jsx.
Steps to Create the Backend Server:
Step 1: Create a directory for project
mkdir Backend
cd Backend
Step 2: Create a server using the following command in your terminal.
npm init -yStep 3: Install the required dependencies in your server using the following command.
npm i express mongoose cors nodemon jsonwebtokenProject Structure:
The package.json file of backend will look like:
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.2.0"
}
Example: Below is an example of server for creating budget tracker with MERN Stack.
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const authRoutes = require('./routes/auth');
const budgetRoutes = require('./routes/budget');
const cors = require('cors')
const app = express();
const PORT = process.env.PORT || 4000;
app.use(cors())
app.use(bodyParser.json());
mongoose.connect('mongodb://localhost:27017/my-app',
{
useNewUrlParser: true,
useUnifiedTopology: true
});
app.use('/auth', authRoutes);
app.use('/budget', budgetRoutes);
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
// routes/auth.js
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
router.post('/signup', async (req, res) => {
const { name, email, password } = req.body;
try {
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({
message: 'User already exists'
});
}
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new User({
name,
email,
password: hashedPassword
});
await newUser.save();
res.status(201).json({
message: 'User created successfully'
});
} catch (error) {
res.status(500).json({
message: 'Internal server error'
});
}
});
router.post('/login', async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({
message: 'Invalid email or password'
});
}
const passwordMatch = await bcrypt.compare(
password, user.password);
if (!passwordMatch) {
return res.status(400).json({
message: 'Invalid email or password'
});
}
const token = jwt.sign({ userId: user._id },
'your_secret_key');
res.status(200).json({ token });
} catch (error) {
res.status(500).json({
message: 'Internal server error'
});
}
});
module.exports = router;
// routes/budget.js
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const Budget = require('../models/Budget');
const mongoose = require('mongoose');
// Authentication middleware
const authMiddleware = async (req, res, next) => {
const token = req.header('Authorization');
if (!token) return res.status(401).json({
message: 'Access denied'
});
try {
const decoded = jwt.verify(token, 'your_secret_key');
req.user = await User.findById(decoded.userId);
next();
} catch (error) {
res.status(400).json({ message: 'Invalid token' });
}
};
router.get('/:id/expenses', authMiddleware, async (req, res) => {
const { id } = req.params;
try {
const budgets = await Budget.findOne({ _id: id });
console.log(budgets)
if (!budgets) {
return res.status(200).json({ expenses: [] });
}
const { available, remaining, totalExpenses } =
calculateAmounts(budgets);
const data = {
available,
remaining,
used: totalExpenses,
budgets,
name: budgets.name,
total: budgets.totalAmount
}
console.log(data)
res.status(200).json({ data: data });
} catch (error) {
console.log(error)
res.status(500).json({
message: 'Internal server error'
});
}
});
// Create a new budget
router.post('/create', authMiddleware,
async (req, res) => {
const { name, totalAmount } = req.body;
try {
const newBudget = new Budget({
name,
totalAmount,
user: req.user._id,
expenses: [],
});
await newBudget.save();
res.status(201).json(newBudget);
} catch (error) {
res.status(500).json({
message: 'Internal server error'
});
}
});
// Enter an expense for a budget
router.post('/:id/expenses', authMiddleware,
async (req, res) => {
const { id } = req.params;
const { name, amount } = req.body;
try {
const budget = await Budget.findOne({
_id: id, user: req.user._id
});
if (!budget) {
return res.status(404).json({
message: 'Budget not found'
});
}
budget.expenses.push({ name, amount });
await budget.save();
res.status(200).json(budget);
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
});
// Calculate available and remaining amounts for a budget
function calculateAmounts(budget) {
const totalExpenses = budget.expenses.reduce(
(total, expense) => total + expense.amount, 0);
const available = budget.totalAmount - totalExpenses;
const remaining = budget.totalAmount - available;
return { available, remaining, totalExpenses };
}
router.get('/', authMiddleware, async (req, res) => {
try {
const budgets = await Budget.find({ user: req.user._id });
const budgetsWithAmounts = budgets.map(budget => {
const { available, remaining } = calculateAmounts(budget);
const used = budget.totalAmount - available;
return {
_id: budget._id,
name: budget.name,
totalAmount: budget.totalAmount,
available,
remaining,
used,
user: budget.user
};
});
res.status(200).json(budgetsWithAmounts);
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
});
module.exports = router;
// models/Budget.js
const mongoose = require('mongoose');
const expenseSchema = new mongoose.Schema({
name: String,
amount: Number,
});
const budgetSchema = new mongoose.Schema({
user: String,
name: String,
totalAmount: Number,
expenses: [expenseSchema],
});
module.exports = mongoose.model('Budget', budgetSchema);
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: String,
email: String,
password: String
});
module.exports = mongoose.model('User', userSchema);
Start your server using the following command.
npm start
Steps to Create the Frontend Application:
Step 1: Initialized the React App with Vite and installing the required packages
npm create vite@latest -y
->Enter Project name: "Frontend"
->Select a framework: "React"
->Select a Variant: "Javascript"
cd Frontend
npm install
Project Structure:
The package.json file of frontend will look like:
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"react-router-dom": "^6.22.2"
},
"devDependencies": {
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"vite": "^5.1.4"
}
}
Example: Below is an example of frontend for creating budget tracker with MERN Stack.
/* src/index.css */
* {
margin: 0;
padding: 0;
}
.first {
width: 50%;
height: 1000px;
float: left;
}
.second {
width: 50%;
height: 1000px;
float: right;
}
.input1 {
width: 82%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid green;
border-radius: 4px;
box-sizing: border-box;
text-align: center;
margin-left: 3%;
}
.btn1 {
width: 10%;
background-color: #4CAF50;
color: white;
padding: 12px 20px;
margin: 8px 0;
border: none;
border-radius: 4px;
cursor: pointer;
margin-left: 1%;
}
.text1 {
text-align: center;
margin: 2%;
}
.input2 {
width: 90%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
text-align: center;
margin-left: 5%;
}
.label1 {
margin-left: 6%;
font-size: large;
}
.btn2 {
width: 10%;
background-color: blue;
color: white;
padding: 12px 20px;
margin: 8px 0;
border: none;
border-radius: 4px;
cursor: pointer;
margin-left: 85%;
}
.text2 {
text-align: center;
margin-top: 10%;
color: blue;
}
.div2 {
display: flex;
justify-content: center;
}
.budget {
width: 55%;
background-color: rgb(149, 0, 255);
color: white;
padding: 12px 20px;
text-align: center;
border-radius: 15px;
margin-top: 2%;
margin-left: 5%;
}
.left {
width: 55%;
background-color: rgb(0, 255, 132);
color: white;
padding: 12px 20px;
margin-top: 2%;
margin-left: 5%;
text-align: center;
border-radius: 15px;
font-weight: bolder;
}
.old ul {
list-style-type: none;
width: 60%;
}
.old li {
display: block;
color: black;
padding: 16px;
border: 1px solid #ccc;
border-radius: 4px;
margin: 1%;
width: 100%;
display: flex;
justify-content: space-between;
}
.name1 {
border-radius: 5px;
background-color: rgba(0, 0, 255, 0.701);
color: white;
}
.g1 {
padding: 35px;
font-size: 30px;
color: green;
font-weight: bolder;
}
* {
margin: 0;
padding: 0;
}
.float-container {
border: 3px solid #fff;
padding: 20px;
display: flex;
justify-content: center;
}
.first-child {
width: 50%;
float: left;
padding: 20px;
border: 2px solid green;
}
.input_exp {
width: 80%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin-left: 10%;
}
.btn_exp {
width: 80%;
background-color: #4CAF50;
color: white;
padding: 14px 20px;
margin: 8px 0;
border: none;
border-radius: 4px;
cursor: pointer;
margin-left: 10%;
}
.btn_exp:hover {
background-color: #45a049;
}
.second-child {
width: 50%;
float: right;
padding: 20px;
border: 2px solid green;
}
.newul {
list-style-type: none;
}
.li_exp {
display: block;
color: black;
padding: 16px;
border: 1px solid #ccc;
border-radius: 4px;
width: 75%;
display: flex;
margin-left: 10%;
margin-top: 3%;
justify-content: space-between;
}
.text_exp {
color: blue;
text-align: center;
}
.name_exp {
border-radius: 5px;
color: black;
font-weight: bolder;
}
.outer_div {
display: flex;
justify-content: center;
}
.outer_btn {
width: 40%;
background-color: rgb(149, 0, 255);
color: white;
padding: 12px 20px;
text-align: center;
border-radius: 15px;
margin: 25px;
}
.logo {
padding: 20px;
color: green;
}
/* LoginForm.css */
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.login-form {
width: 350px;
padding: 30px;
border: 1px solid #ccc;
border-radius: 5px;
padding-right: 30px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.login-form input {
width: 100%;
margin-bottom: 10px;
padding: 15px;
border: 1px solid #ccc;
border-radius: 3px;
}
.login-form button {
width: 100%;
padding: 15px;
background-color: #00ff04;
border: none;
border-radius: 3px;
color: #fff;
font-size: 16px;
cursor: pointer;
}
.login-form button:hover {
background-color: #00b374;
}
.p1 {
padding: 15px;
text-align: center;
}
* {
margin: 0;
padding: 0;
}
.main1 {
display: flex;
justify-content: center;
padding-top: 5%;
}
.input1 {
width: 100%;
margin: 8px 0;
display: inline-block;
border: 1px solid green;
border-radius: 4px;
box-sizing: border-box;
text-align: center;
}
.btn1 {
width: 15%;
background-color: #4CAF50;
color: white;
padding: 12px 20px;
margin: 8px 0;
border: none;
border-radius: 10px;
cursor: pointer;
display: inline-block;
}
.f1 {
width: 60%;
}
.grid {
display: grid;
height: 200px;
width: 100%;
gap: 20px;
justify-content: center;
grid-template-columns: auto auto auto;
margin-top: 30px;
}
.inner1 {
border-radius: 10px;
padding: 52px;
border: 5px solid rgba(128, 128, 128, 0.55);
}
.in1 {
width: auto;
border: 5px ridge gray;
color: black;
padding: 5px;
}
.in2 {
width: auto;
border: 5px ridge gray;
color: black;
padding: 5px;
margin-top: 10px;
}
.btn_exp {
background-color: #04AA6D;
border: none;
color: white;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 13px;
cursor: pointer;
}
.registration-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.p1 {
padding: 15px;
text-align: center;
}
.registration-form {
width: 400px;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.registration-form input {
width: 100%;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 3px;
}
.registration-form button {
width: 100%;
padding: 10px;
background-color: #00ff04;
border: none;
border-radius: 3px;
color: #fff;
font-size: 16px;
cursor: pointer;
}
.registration-form button:hover {
background-color: #00b374;
}
// src/main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import LoginForm from "./component/LoginForm.jsx";
import RegistrationForm from "./component/RegistrationForm.jsx";
import {
BrowserRouter,
Routes,
Route
} from "react-router-dom";
import Home from "./component/Home.jsx";
import Expense from "./component/Expense.jsx";
ReactDOM.createRoot(document.getElementById("root")).render(
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<LoginForm />} />
<Route path="/register" element={<RegistrationForm />} />
<Route path="/Expense/:id" element={<Expense />} />
</Routes>
</BrowserRouter>
);
// src/component/Expense.jsx
import React, {
useEffect,
useState
} from "react";
import { MdDeleteForever } from "react-icons/md";
import {
useNavigate,
useParams
} from "react-router-dom";
function Expense(props) {
const navigate = useNavigate();
useEffect(() => {
if (!localStorage.getItem('token')) {
navigate("/login");
}
}, [])
let { id } = useParams();
const [expenses, setExpenses] = useState({});
const [expAdd, setExpAdd] = useState(true);
const [name, setName] = useState('');
const [amount, setAmount] = useState('');
async function fetchExpenses() {
try {
const response = await
fetch(`http://localhost:4000/budget/${id}/expenses`, {
headers: {
Authorization: localStorage.getItem('token'),
},// Assuming token is stored in local storage
});
if (!response.ok) {
throw new Error('Failed to fetch expenses');
}
const data = await response.json();
console.log(data.data)
if (data.data) {
setExpenses(data.data);
}
} catch (error) {
console.log(error.message)
console.error('Error fetching expenses:', error);
}
};
useEffect(() => {
fetchExpenses();
}, [id]); // Runs whenever budgetId changes
useEffect(() => {
fetchExpenses();
}, [expAdd]); // Runs whenever budgetId changes
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await
fetch(`http://localhost:3000/budget/${id}/expenses`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: localStorage.getItem('token'),
},// Assuming token is stored in local storage
body: JSON.stringify({ name, amount }),
});
if (!response.ok) {
throw new Error('Failed to add expense');
}
const data = await response.json();
console.log('Expense added:', data);
alert("added")
setExpAdd(!expAdd)
/*
Handle success: e.g.,
show a success message or update the UI
*/
} catch (error) {
console.error('Error adding expense:', error);
// Handle error: e.g., show an error message to the user
}
};
return (
<>
<div><h1 className="logo">
<u>GFG Budget Tracker</u>
</h1>
</div>
<h3 style={{
marginTop: '40px', marginBottom: '40px',
textAlign: 'center'
}}>
Budget Name :{expenses.name}</h3>
<div className="float-container">
<div className="first-child">
<form className="form_exp"
onSubmit={handleSubmit}>
<input
type="text"
placeholder="Expense Name..."
className="input_exp"
value={name} onChange={
(e) => setName(e.target.value)}
/>
<input type="text" placeholder="Amount"
value={amount} onChange={
(e) => setAmount(e.target.value)}
className="input_exp" />
<input type="submit" value="Add"
className="btn_exp" />
</form>
</div>
<div className="second-child">
<h1 className="text_exp">List of Expenses</h1>
<ul className="newul">
{expenses?.budgets?.expenses?.map((item) => {
return (
<li className="li_exp">
<span className="name_exp">
{item.name}
</span>
<span className="a_exp">
{item.amount}
</span>
</li>
)
})}
</ul>
</div>
</div>
<div className="outer_div">
<div className="outer_btn">
Budget:{expenses.total}
</div>
<div className="outer_btn" style={{
backgroundColor: "red"
}}>
Used:{expenses.used}
</div>
<div className="outer_btn" style={{
backgroundColor: "green"
}}>
Left:{expenses.available}
</div>
</div>
</>
);
}
export default Expense;
// src/component/Home.jsx
import React, {
useEffect,
useState
} from 'react'
import {
Link,
useNavigate
} from 'react-router-dom';
function Home() {
const [budget, setBudgets] = useState([]);
const [name, setName] = useState('')
const [amount, setAmount] = useState(0)
const [budAdd, setAddBud] = useState(true);
const navigate = useNavigate();
const token = localStorage.getItem('token');
async function fetchBudgets() {
try {
const response = await
fetch('http://localhost:4000/budget', {
headers: {
Authorization: token,
},
});
if (!response.ok) {
throw new Error('Failed to fetch budgets');
}
const data = await response.json();
console.log(data)
setBudgets(data);
} catch (error) {
console.error('Error fetching budgets:', error);
}
};
useEffect(() => {
if (!localStorage.getItem('token')) {
navigate("/login")
}
fetchBudgets();
}, []); // Runs only once on component mount
useEffect(() => {
fetchBudgets();
}, [budAdd]); // Runs only once on component mount
const handleSubmit = async (e) => {
e.preventDefault();
try {
const token = localStorage.getItem('token');
console.log(token)
const response = await
fetch('http://localhost:4000/budget/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: token,
},
body: JSON.stringify({ name, totalAmount: amount }),
});
const data = await response.json();
console.log('Budget created:', data);
alert("Budget created")
setAddBud(!budAdd)
} catch (error) {
console.error('Error creating budget:', error);
alert("Error creating budget")
}
};
return (
<>
<div><h1 className="logoHome"
style={{
padding: '20px',
marginLeft: '19%', color: 'green'
}}>
<u>GFG Budget Tracker</u>
</h1>
</div>
<div className='main1'>
<form className='f1'
onSubmit={handleSubmit} method='POST'>
<input type="text"
min="1"
className='input1'
placeholder='Enter Expense Name'
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input type="number"
min="1"
className='input1'
placeholder='Input Amount'
value={amount}
onChange={(e) => setAmount(e.target.value)}
/>
<br />
<input type="submit"
value="Add"
className='btn1'
/>
</form>
</div>
<div className='grid'>
{budget?.map((bud, index) => (
<div className='inner1'>
<div className='in1' >
Expense Name : {bud.name}
</div>
<div className='in2'>
Amount : {bud.totalAmount}
</div>
<Link to={`expense/${bud._id}`}>
<button className='btn_exp'>
Open Budget
</button>
</Link>
</div>
))}
</div>
</>
)
}
export default Home
// src/component/LoginForm.jsx
import React, { useState } from 'react';
import {
Link,
useNavigate
} from "react-router-dom";
const LoginForm = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await
fetch('http://localhost:4000/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: username,
password
})
});
const data = await response.json();
alert("Login Success")
localStorage.setItem('token', data.token);
navigate("/")
} catch (error) {
console.error('Error:', error);
alert('Error:', error)
}
};
return (
<>
<div className="login-container">
<form onSubmit={handleSubmit}
className="login-form">
<input
type="text"
placeholder="Username"
value={username}
onChange={
(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={
(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
<p className='p1'>
Don't Have an Account
<Link to="/register">
Sign Up
</Link>
</p>
</form>
</div>
</>
);
};
export default LoginForm;
// src/component/RegistrationForm.jsx
import React, { useState } from 'react';
import { Link, useNavigate } from "react-router-dom";
const RegistrationForm = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await
fetch('http://localhost:4000/auth/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
email,
password
})
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('token', data.token);
alert('Success:', data.message)
navigate("/login");
} else {
console.error(data.message);
alert('Error:', data.message)
}
} catch (error) {
console.error('Error:', error);
alert('Error:', error)
}
};
return (
<div className="registration-container">
<form onSubmit={handleSubmit}
className="registration-form">
<input
type="text"
placeholder="Name"
value={name}
className='i1'
onChange={
(e) => setName(e.target.value)}
required
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={
(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={
(e) => setPassword(e.target.value)}
required
/>
<button type="submit">Register</button>
<p className='p1'>
Alreay Account
<Link to="/login">
Sign In
</Link>
</p>
</form>
</div>
);
};
export default RegistrationForm;
Start your frontend application using the following command.
npm run devOutput:
- Browser Output:

- Output of data saved in Database:
