Budget Tracking App with Node.js and Express.js
In this article, we’ll walk through the step-by-step process of creating a Budget Tracking App with Node.js and Express.js. This application will provide users with the ability to track their income, expenses, and budgets. It will allow users to add, delete, and view their income and expenses, as well as set budgets for different expense categories. Additionally, users will be able to generate reports to analyze their financial data over time.
Prerequisites:
Approach to Create a Budget Tracking App with Node.js and Express.js
- Set up a database connection using Mongoose
- Implement user authentication using JWT (JSON Web Tokens) or sessions.
- Create routes and corresponding controllers for user registration, login, logout, and authentication middleware.
- Define Mongoose models for incomes and implement CRUD operations for managing user incomes.
- Define Mongoose models for expenses and handle CRUD operations.
- Define Mongoose models for budgets and handle CRUD operations.
- Implement routes and controllers for generating reports like monthly expense reports and income vs. expense analysis by aggregating data.
- Set up middleware functions for tasks such as request logging, error handling, and authentication verification.
- Implement error handling middleware to catch and respond to errors gracefully.
- Use validation libraries like express-validator to validate incoming request data for routes related to income, expenses, and budgets to ensure data integrity.
Steps to Create the NodeJS App and Installing Module
Step 1: Create a NodeJS project using the following command.
npm init -y
Step 2: Install Express.js and other necessary dependencies.
npm install express bcrypt body-parser express-validator jsonwebtoken mongoose nodemon
Step 3: Create your routes, controllers, models, middleware, and other components as needed for your application logic.
Step 4: Create a files in each folder as shown in project folder structure.
Project Structure:
Dependencies:
"dependencies": {
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"express": "^4.19.2",
"express-validator": "^7.0.1",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.2.4",
"nodemon": "^3.1.0"
}
Example: Write the following code in respective files
// config.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect('mongodb+srv://admin:<password>@cluster0.fcugwoe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('Connected to MongoDB');
} catch (error) {
console.error('Failed to connect to MongoDB:', error);
}
};
module.exports = {
mongoURI: process.env.MONGO_URI || 'mongodb+srv://admin:admin123@cluster0.fcugwoe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0',
jwtSecret: process.env.JWT_SECRET || 'your_jwt_secret',
email: {
host: process.env.EMAIL_HOST || 'smtp.example.com',
port: process.env.EMAIL_PORT || 587,
user: process.env.EMAIL_USER || 'your_email@example.com',
pass: process.env.EMAIL_PASS || 'your_email_password'
},
connectDB,
};
// middleware.js
const jwt = require('jsonwebtoken');
const config = require('./config');
// Middleware function to verify JWT token
function verifyToken(req, res, next) {
// Get token from header
const token = req.header('Authorization');
// Check if token is present
if (!token) {
return res.status(401).json({
msg: 'No token, authorization denied'
});
}
try {
// Verify token
const decoded = jwt.verify(token, config.jwtSecret);
// Add user from payload
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ msg: 'Token is not valid' });
}
}
module.exports = verifyToken;
// authController.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const config = require('../config/config');
// const User = require('../src/models/user.js');
const User = require('../models/user');
// Controller function for user registration
async function register(req, res) {
const { username, email, password } = req.body;
try {
// Check if user already exists
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({
msg: 'User already exists'
});
}
// Create new user
user = new User({ username, email, password });
// Hash password
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
// Save user to database
await user.save();
res.status(201).json({
msg: 'User registered successfully'
});
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
// Controller function for user login
async function login(req, res) {
const { email, password } = req.body;
try {
// Check if user exists
let user = await User.findOne({ email });
if (!user) {
return res.status(400).json({
msg: 'Invalid credentials'
});
}
// Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({
msg: 'Invalid credentials'
});
}
// Generate JWT token
const payload = {
user: {
id: user.id
}
};
jwt.sign(payload, config.jwtSecret, {
expiresIn: '1h'
}, (err, token) => {
if (err) throw err;
res.json({ token });
});
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
async function getUser(req, res) {
try {
// Fetch user data based on the user ID from the token
const user = await User.findById(req.user.id).select('-password');
res.json(user);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
module.exports = { register, login, getUser };
// budgetController.js
const Budget = require('../models/budget');
// Controller function to get user's budget
async function getUserBudget(req, res) {
try {
const budget = await Budget.findOne({
user: req.user.id
});
if (!budget) {
return res.status(404).json({
msg: 'Budget not found'
});
}
res.json(budget);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
// Controller function to update user's budget
async function updateUserBudget(req, res) {
const { categories } = req.body;
try {
let budget = await Budget.findOne({
user: req.user.id
});
if (!budget) {
// Create new budget if not exists
budget = new Budget({
user: req.user.id,
categories
});
await budget.save();
return res.status(201).json(budget);
}
// Update existing budget
budget.categories = categories;
await budget.save();
res.json(budget);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
module.exports = { getUserBudget, updateUserBudget };
// expenseController.js
const Expense = require('../models/expense');
// Controller function to get all expenses
async function getAllExpenses(req, res) {
try {
const expenses = await Expense.find({
user: req.user.id
}).sort({ date: -1 });
res.json(expenses);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
// Controller function to add a new expense
async function addExpense(req, res) {
const { amount, category, description } = req.body;
try {
const newExpense = new Expense({
amount,
category,
description,
user: req.user.id
});
const expense = await newExpense.save();
res.status(201).json(expense);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
// Controller function to delete an expense
async function deleteExpense(req, res) {
try {
let expense = await Expense.findById(req.params.id);
if (!expense) {
return res.status(404).json({
msg: 'Expense not found'
});
}
// Check if user owns the expense
if (expense.user.toString() !== req.user.id) {
return res.status(401).json({
msg: 'Not authorized'
});
}
await expense.remove();
res.json({ msg: 'Expense removed' });
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
module.exports = { getAllExpenses, addExpense, deleteExpense };
// incomeController.js
const Income = require('../models/income');
// Controller function to get all incomes
async function getAllIncomes(req, res) {
try {
const incomes = await Income.find({
user: req.user.id
}).sort({ date: -1 });
res.json(incomes);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
// Controller function to add a new income
async function addIncome(req, res) {
const { amount, description } = req.body;
try {
// Use req.user.id to attach user ID to the income object
const newIncome = new Income({
amount,
description,
user: req.user.id // Attach user ID to the income object
});
const income = await newIncome.save();
res.status(201).json(income);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
// Controller function to delete an income
async function deleteIncome(req, res) {
try {
let income = await Income.findById(req.params.id);
if (!income) {
return res.status(404).json({
msg: 'Income not found'
});
}
// Check if user owns the income
if (income.user.toString() !== req.user.id) {
return res.status(401).json({
msg: 'Not authorized'
});
}
await income.remove();
res.json({ msg: 'Income removed' });
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
module.exports = { getAllIncomes, addIncome, deleteIncome };
JavaScript// reportController.js
const Expense = require('../models/expense');
const Budget = require('../models/budget');
// Controller function to generate expense report
async function generateExpenseReport(req, res) {
try {
// Get expenses for the authenticated user
const expenses = await Expense.find({
user: req.user.id
});
// Calculate total expenses
const totalExpenses = expenses.reduce(
(total, expense) => total + expense.amount, 0);
// Get user's budget
const budget = await Budget.findOne({
user: req.user.id
});
// Calculate remaining budget
let remainingBudget = 0;
if (budget) {
const { categories } = budget;
const budgetAmounts = categories.map(category => category.amount);
const totalBudget = budgetAmounts.reduce((total, amount) => total + amount, 0);
remainingBudget = totalBudget - totalExpenses;
}
res.json({
totalExpenses,
remainingBudget
});
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
}
module.exports = { generateExpenseReport };
// budget.js
const mongoose = require('mongoose');
const BudgetSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
// required: true
},
categories: [
{
name: {
type: String,
required: true
},
amount: {
type: Number,
required: true
}
}
]
});
const Budget = mongoose.model('Budget', BudgetSchema);
module.exports = Budget;
// expense.js
const mongoose = require('mongoose');
const ExpenseSchema = new mongoose.Schema({
amount: {
type: Number,
required: true
},
category: {
type: String,
required: true
},
description: {
type: String
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
// required: true
},
date: {
type: Date,
default: Date.now
}
});
const Expense = mongoose.model('Expense', ExpenseSchema);
module.exports = Expense;
// income.js
const mongoose = require('mongoose');
const IncomeSchema = new mongoose.Schema({
amount: {
type: Number,
required: true
},
description: {
type: String
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
// required: true
},
date: {
type: Date,
default: Date.now
}
});
const Income = mongoose.model('Income', IncomeSchema);
module.exports = Income;
// user.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});
const User = mongoose.model('User', UserSchema);
module.exports = User;
// authRoutes.js
const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const authController = require('../controllers/authController');
const verifyToken = require('../config/middleware');
// Route: POST /api/auth/register
// Description: Register a new user
router.post('/register', [
body('username', 'Username is required').notEmpty(),
body('email', 'Please include a valid email').isEmail(),
body('password', 'Please enter a password with 6 or more characters').isLength({ min: 6 })
], authController.register);
// Route: POST /api/auth/login
// Description: Authenticate user & get token (login)
router.post('/login', [
body('email', 'Please include a valid email').isEmail(),
body('password', 'Password is required').exists()
], authController.login);
// Route: GET /api/auth/user
// Description: Get user data (protected route)
router.get('/user', verifyToken, authController.getUser);
module.exports = router;
// budgetRoutes.js
const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const budgetController = require('../controllers/budgetController');
const verifyToken = require('../config/middleware');
// Route: GET /api/budget
// Description: Get user's budget
router.get('/', verifyToken, budgetController.getUserBudget);
// Route: POST /api/budget
// Description: Update user's budget
router.post('/', verifyToken, budgetController.updateUserBudget);
module.exports = router;
// expenseRoutes.js
const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const expenseController = require('../controllers/expenseController');
const verifyToken = require('../config/middleware');
// Route: GET /api/expenses
// Description: Get all expenses
router.get('/', verifyToken, expenseController.getAllExpenses);
// Route: POST /api/expenses
// Description: Add a new expense
router.post('/', verifyToken, [
body('amount', 'Amount is required').notEmpty(),
body('category', 'Category is required').notEmpty()
], expenseController.addExpense);
// Route: DELETE /api/expenses/:id
// Description: Delete an expense
router.delete('/:id', verifyToken, expenseController.deleteExpense);
module.exports = router;
// incomeRoutes.js
const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const incomeController = require('../controllers/incomeController');
const verifyToken = require('../config/middleware');
// Route: GET /api/incomes
// Description: Get all incomes
router.get('/', verifyToken, incomeController.getAllIncomes);
// Route: POST /api/incomes
// Description: Add a new income
router.post('/', verifyToken, [
body('amount', 'Amount is required').notEmpty(),
body('description', 'Description is required').notEmpty()
// Add validation for description if required
], incomeController.addIncome);
// Route: DELETE /api/incomes/:id
// Description: Delete an income
router.delete('/:id', verifyToken, incomeController.deleteIncome);
module.exports = router;
// reportRoutes.js
const express = require('express');
const router = express.Router();
const reportController = require('../controllers/reportController');
const verifyToken = require('../config/middleware');
// Route: GET /api/report/expense
// Description: Generate expense report
router.get('/expense', verifyToken, reportController.generateExpenseReport);
module.exports = router;
// authService.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const config = require('../config/config');
const { validationResult } = require('express-validator');
const User = require('../models/user');
// Function to register a new user
async function registerUser(username, email, password) {
try {
// Check if user already exists
let user = await User.findOne({ email });
if (user) {
return { error: 'User already exists' };
}
// Create new user
user = new User({ username, email, password });
// Hash password
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
// Save user to database
await user.save();
return { message: 'User registered successfully' };
} catch (err) {
console.error(err.message);
return { error: 'Server Error' };
}
}
// Function to authenticate user and generate JWT token
async function authenticateUser(email, password) {
try {
// Check if user exists
let user = await User.findOne({ email });
if (!user) {
return { error: 'Invalid credentials' };
}
// Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return { error: 'Invalid credentials' };
}
// Generate JWT token
const payload = {
user: {
id: user.id
}
};
const token = jwt.sign(payload, config.jwtSecret, { expiresIn: '1h' });
return { token };
} catch (err) {
console.error(err.message);
return { error: 'Server Error' };
}
}
module.exports = { registerUser, authenticateUser };
// budgetService.js
const Budget = require('../models/budget');
// Function to get user's budget
async function getUserBudget(userId) {
try {
const budget = await Budget.findOne({ user: userId });
return budget;
} catch (err) {
console.error(err.message);
throw new Error('Server Error');
}
}
// Function to update user's budget
async function updateUserBudget(categories, userId) {
try {
let budget = await Budget.findOne({ user: userId });
if (!budget) {
// Create new budget if not exists
budget = new Budget({
user: userId,
categories
});
await budget.save();
return budget;
}
// Update existing budget
budget.categories = categories;
await budget.save();
return budget;
} catch (err) {
console.error(err.message);
throw new Error('Server Error');
}
}
module.exports = { getUserBudget, updateUserBudget };
// expenseService.js
const Expense = require('../models/expense');
// Function to get all expenses for a user
async function getAllExpenses(userId) {
try {
const expenses = await Expense.find({
user: userId
}).sort({ date: -1 });
return expenses;
} catch (err) {
console.error(err.message);
throw new Error('Server Error');
}
}
// Function to add a new expense
async function addExpense(amount, category, description, userId) {
try {
const newExpense = new Expense({
amount,
category,
description,
user: userId
});
const expense = await newExpense.save();
return expense;
} catch (err) {
console.error(err.message);
throw new Error('Server Error');
}
}
// Function to delete an expense
async function deleteExpense(expenseId, userId) {
try {
let expense = await Expense.findById(expenseId);
if (!expense) {
throw new Error('Expense not found');
}
// Check if user owns the expense
if (expense.user.toString() !== userId) {
throw new Error('Not authorized');
}
await expense.remove();
} catch (err) {
console.error(err.message);
throw err;
}
}
module.exports = { getAllExpenses, addExpense, deleteExpense };
// incomeService.js
const Income = require('../models/income');
// Function to get all incomes for a user
async function getAllIncomes(userId) {
try {
const incomes = await Income.find({ user: userId }).sort({ date: -1 });
return incomes;
} catch (err) {
console.error(err.message);
throw new Error('Server Error');
}
}
// Function to add a new income
async function addIncome(amount, description, userId) {
try {
const newIncome = new Income({
amount,
description,
user: userId
});
const income = await newIncome.save();
return income;
} catch (err) {
console.error(err.message);
throw new Error('Server Error');
}
}
// Function to delete an income
async function deleteIncome(incomeId, userId) {
try {
let income = await Income.findById(incomeId);
if (!income) {
throw new Error('Income not found');
}
// Check if user owns the income
if (income.user.toString() !== userId) {
throw new Error('Not authorized');
}
await income.remove();
} catch (err) {
console.error(err.message);
throw err;
}
}
module.exports = { getAllIncomes, addIncome, deleteIncome };
// reportService.js
const Expense = require('../models/expense');
const Budget = require('../models/budget');
// Function to generate expense report
async function generateExpenseReport(userId) {
try {
// Get expenses for the user
const expenses = await Expense.find({
user: userId
});
// Calculate total expenses
const totalExpenses = expenses.reduce((total, expense) => total + expense.amount, 0);
// Get user's budget
const budget = await Budget.findOne({ user: userId });
// Calculate remaining budget
let remainingBudget = 0;
if (budget) {
const { categories } = budget;
const budgetAmounts = categories.map(category => category.amount);
const totalBudget = budgetAmounts.reduce((total, amount) => total + amount, 0);
remainingBudget = totalBudget - totalExpenses;
}
return { totalExpenses, remainingBudget };
} catch (err) {
console.error(err.message);
throw new Error('Server Error');
}
}
module.exports = { generateExpenseReport };
// errorHandler.js
// Function to handle errors and send appropriate response
function errorHandler(err, req, res, next) {
console.error(err.stack);
const statusCode = res.statusCode !== 200 ? res.statusCode : 500;
res.status(statusCode).json({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? '?' : err.stack
});
}
module.exports = errorHandler;
// notification.js
// Function to send email notification
function sendEmailNotification(email, message) {
// Implement logic to send email notification using a third-party service or library
console.log(`Email notification sent to ${email}: ${message}`);
}
// Function to send SMS notification
function sendSMSNotification(phoneNumber, message) {
// Implement logic to send SMS notification using a third-party service or library
console.log(`SMS notification sent to ${phoneNumber}: ${message}`);
}
module.exports = { sendEmailNotification, sendSMSNotification };
// validation.js
const { body } = require('express-validator');
// Validation rules for user registration
const registerValidationRules = () => {
return [
body('username', 'Username is required').notEmpty(),
body('email', 'Please include a valid email').isEmail(),
body('password', 'Please enter a password with 6 or more characters').isLength({ min: 6 })
];
};
// Validation rules for user login
const loginValidationRules = () => {
return [
body('email', 'Please include a valid email').isEmail(),
body('password', 'Password is required').exists()
];
};
module.exports = { registerValidationRules, loginValidationRules };
// app.js
const express = require('express');
const { connectDB } = require('./config/config.js'); // Import connectDB from config.js
const errorHandler = require('./utils/errorHandler.js');
const authRoutes = require('./routes/authRoutes.js');
const expenseRoutes = require('./routes/expenseRoutes.js');
const incomeRoutes = require('./routes/incomeRoutes.js');
const budgetRoutes = require('./routes/budgetRoutes.js');
const reportRoutes = require('./routes/reportRoutes.js');
// Initialize Express app
const app = express();
// Connect to MongoDB
connectDB(); // Call connectDB function
// Middleware
app.use(express.json());
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/expenses', expenseRoutes);
app.use('/api/incomes', incomeRoutes);
app.use('/api/budget', budgetRoutes);
app.use('/api/report', reportRoutes);
// Error handler middleware
app.use(errorHandler);
// Define port
const PORT = process.env.PORT || 5000;
// Start server
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`);
});
Start your server using the following command.
node app.js
Output: