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 -y
Step 3: Install the required dependencies in your server using the following command.
npm i express mongoose cors nodemon jsonwebtoken
Project 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()); {
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 ;
} .input 1 {
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% ;
} .btn 1 {
width : 10% ;
background-color : #4CAF50 ;
color : white ;
padding : 12px 20px ;
margin : 8px 0 ;
border : none ;
border-radius: 4px ;
cursor : pointer ;
margin-left : 1% ;
} .text 1 {
text-align : center ;
margin : 2% ;
} .input 2 {
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% ;
} .label 1 {
margin-left : 6% ;
font-size : large ;
} .btn 2 {
width : 10% ;
background-color : blue ;
color : white ;
padding : 12px 20px ;
margin : 8px 0 ;
border : none ;
border-radius: 4px ;
cursor : pointer ;
margin-left : 85% ;
} .text 2 {
text-align : center ;
margin-top : 10% ;
color : blue ;
} .div 2 {
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;
} .name 1 {
border-radius: 5px ;
background-color : rgba( 0 , 0 , 255 , 0.701 );
color : white ;
} .g 1 {
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 : 100 vh;
} .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 ;
} .p 1 {
padding : 15px ;
text-align : center ;
} * { margin : 0 ;
padding : 0 ;
} .main 1 {
display : flex;
justify- content : center ;
padding-top : 5% ;
} .input 1 {
width : 100% ;
margin : 8px 0 ;
display : inline- block ;
border : 1px solid green ;
border-radius: 4px ;
box-sizing: border-box;
text-align : center ;
} .btn 1 {
width : 15% ;
background-color : #4CAF50 ;
color : white ;
padding : 12px 20px ;
margin : 8px 0 ;
border : none ;
border-radius: 10px ;
cursor : pointer ;
display : inline- block ;
} .f 1 {
width : 60% ;
} .grid { display : grid;
height : 200px ;
width : 100% ;
gap: 20px ;
justify- content : center ;
grid-template-columns: auto auto auto ;
margin-top : 30px ;
} .inner 1 {
border-radius: 10px ;
padding : 52px ;
border : 5px solid rgba( 128 , 128 , 128 , 0.55 );
} .in 1 {
width : auto ;
border : 5px ridge gray ;
color : black ;
padding : 5px ;
} .in 2 {
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 : 100 vh;
} .p 1 {
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
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
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
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
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 dev
Output:
- Browser Output:
- Output of data saved in Database: