Expense Management System using MERN Stack
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.
Javascript
// 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}`); }); |
Javascript
// 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; |
Javascript
// 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; |
Javascript
// 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); |
Javascript
// 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.
CSS
/* 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 ; } |
Javascript
// 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> ); |
Javascript
// 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; |
Javascript
// 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 |
Javascript
// 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; |
Javascript
// 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 dev
Output:
- Browser Output:
- Output of data saved in Database: