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:

Project Folder 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

JavaScript
// 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,
};
JavaScript
// 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;
JavaScript
// 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 };
JavaScript
// 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 };
JavaScript
// 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 };
JavaScript
// 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
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 };
JavaScript
// 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;
JavaScript
// 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;
JavaScript
// 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;
JavaScript
// 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;
JavaScript
// 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;
JavaScript
// 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;
JavaScript
// 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;
JavaScript
// 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;
JavaScript
// 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;
JavaScript
// 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 };
JavaScript
// 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 };
JavaScript
// 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 };
JavaScript
// 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 };
JavaScript
// 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 };
JavaScript
// 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;
JavaScript
// 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 };
JavaScript
// 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 };
JavaScript
// 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: