Personal Expense Tracker using React and Spring Boot

Last Updated : 14 Oct, 2026

A Personal Expense Tracker using React and Spring Boot is a full-stack web application that helps users record, organize, and monitor their daily expenses. React provides an interactive interface for managing expenses, while Spring Boot handles REST APIs, calculations, and database operations.

  • Users can add, view, edit, and delete expenses with details such as amount, category, date, and description.
  • Expenses are stored in a MySQL database using Spring Data JPA.
  • The application displays monthly spending totals and category-wise expense summaries to help users understand their spending habits.

In this project, we will build a Personal Expense Tracker using React, Spring Boot, Spring Data JPA, MySQL, and Axios.

Features of the Personal Expense Tracker

The application provides the following features:

  • Add Expenses: Record an expense with an amount, category, date, and description.
  • View Expenses: Display recorded expenses in a structured table.
  • Edit Expenses: Update the details of an existing expense.
  • Delete Expenses: Remove incorrect or unwanted expense records.
  • Category Management: Organize expenses into categories such as Food, Travel, Shopping, Bills, and Others.
  • Monthly Summary: Calculate total expenses for a selected month.
  • Category-wise Summary: Display how much money was spent in each category.
  • Filter Expenses: View expenses by month and category.

How the Personal Expense Tracker Works

The application uses a client-server architecture, where React handles the frontend, Spring Boot processes requests, and MySQL stores expense records.

frame_1000001876

Working Process:

  1. The user enters expense details or selects a reporting month in the React application.
  2. React sends an HTTP request to the Spring Boot backend using Axios.
  3. The REST Controller receives the request and passes it to the Service Layer.
  4. The Service Layer validates and processes expense operations or calculates summaries.
  5. Spring Data JPA retrieves or modifies records in MySQL.
  6. The backend returns JSON data, and React updates the expense dashboard.

Prerequisites

Before starting, install the following:

The current Vite documentation specifies Node.js 20.19+ or 22.12+ as minimum supported release lines. Using a current compatible Node.js LTS version is recommended.

Backend Development using Spring Boot

We will first create the Spring Boot project, configure the database, and develop REST APIs for managing expenses.

Step 1: Create a Spring Boot Project

Open Spring Initializr and select the following project configuration.

  • Project: Maven
  • Language: Java
  • Spring Boot: A compatible stable Spring Boot 3.x release
  • Group: com.example
  • Artifact: expense-tracker
  • Packaging: Jar
  • Java: 17

Add the following dependencies:

  • Spring Web
  • Spring Data JPA
  • MySQL Driver
  • Validation

Click Generate, extract the ZIP file, and open the project in your IDE.

Screenshot-2026-10-13-094531

Backend Project Structure

Screenshot-2026-10-13-113826
Creating the Spring Boot backend for the Personal Expense Tracker.

Step 2: Configure the MySQL Database

Open MySQL Workbench and execute the following SQL query:

CREATE DATABASE expensedb;

Now open src/main/resources/application.properties and add the configuration.

File: application.properties

spring.application.name=expense-tracker

# Database Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/expensedb
spring.datasource.username=root
spring.datasource.password=${DB_PASSWORD}

# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

# Server Port
server.port=8080

Set the DB_PASSWORD environment variable to the password of your MySQL user before starting the backend.

Explanation:

  • spring.datasource.url specifies the database connection.
  • spring.datasource.username and password provide the database credentials.
  • spring.jpa.hibernate.ddl-auto=update allows Hibernate to update the development database schema.
  • spring.jpa.show-sql=true prints generated SQL queries in the console.
  • server.port=8080 runs the backend on port 8080.

Note: The update setting is suitable for local development. Production applications should use database migration tools such as Flyway or Liquibase

Step 3: Create the Expense Entity

The Expense entity represents an individual expense record stored in the database.

Create an entity package and add Expense.java.

File: Expense.java

Java
package com.example.expensetracker.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDate;

@Entity
@Table(name = "expenses")
public class Expense {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @DecimalMin(value = "0.01")
    @Digits(integer = 10, fraction = 2)
    @Column(nullable = false, precision = 12, scale = 2)
    private BigDecimal amount;

    @NotBlank
    @Size(max = 50)
    @Column(nullable = false, length = 50)
    private String category;

    @NotNull
    @Column(nullable = false)
    private LocalDate date;

    @Size(max = 255) private String description;

    public Expense() {}

    public Long getId() { return id; }

    public void setId(Long id) { this.id = id; }

    public BigDecimal getAmount() { return amount; }

    public void setAmount(BigDecimal amount)
    {
        this.amount = amount;
    }

    public String getCategory() { return category; }

    public void setCategory(String category)
    {
        this.category = category;
    }

    public LocalDate getDate() { return date; }

    public void setDate(LocalDate date)
    {
        this.date = date;
    }

    public String getDescription() { return description; }

    public void setDescription(String description)
    {
        this.description = description;
    }
}

Explanation:

  • @Entity maps the Java class to a database table.
  • @Id identifies the primary key.
  • @GeneratedValue automatically generates the expense ID.
  • BigDecimal stores monetary values accurately without the typical floating-point precision issues of double.
  • @DecimalMin and @Digits validate the amount.
  • @NotBlank ensures that a category is provided.
  • LocalDate stores the expense date without a time component.

Step 4: Create the Expense Repository

The Repository Layer communicates with MySQL through Spring Data JPA.

Create a repository package and add ExpenseRepository.java.

File: ExpenseRepository.java

Java
package com.example.expensetracker.repository;

import com.example.expensetracker.entity.Expense;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ExpenseRepository
    extends JpaRepository<Expense, Long> {

    List<Expense> findAllByOrderByDateDescIdDesc();

    List<Expense>
    findByDateGreaterThanEqualAndDateLessThanOrderByDateDesc(
        LocalDate start, LocalDate end);
}

Explanation:

  • JpaRepository provides predefined CRUD methods such as save(), findAll(), findById(), and delete().
  • The additional repository methods retrieve expenses in descending date order and retrieve records belonging to a specified date range.
  • Spring Data JPA generates the repository implementation automatically from the method names.

Step 5: Implement the Expense Service

The Service Layer handles expense-related business logic, including creating, updating, deleting, and calculating monthly spending.

Create a service package and add ExpenseService.java.

File: ExpenseService.java

Java
package com.example.expensetracker.service;

import com.example.expensetracker.entity.Expense;
import com.example.expensetracker.repository.ExpenseRepository;
import java.math.BigDecimal;
import java.time.YearMonth;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

@Service
public class ExpenseService {

    private final ExpenseRepository repository;

    public ExpenseService(ExpenseRepository repository)
    {
        this.repository = repository;
    }

    public List<Expense> getAllExpenses()
    {
        return repository.findAllByOrderByDateDescIdDesc();
    }

    public Expense getExpenseById(Long id)
    {
        return repository.findById(id).orElseThrow(
            ()
                -> new ResponseStatusException(
                    HttpStatus.NOT_FOUND,
                    "Expense not found"));
    }

    public Expense addExpense(Expense expense)
    {
        expense.setId(null);
        return repository.save(expense);
    }

    public Expense updateExpense(Long id, Expense updated)
    {
        Expense expense = getExpenseById(id);

        expense.setAmount(updated.getAmount());
        expense.setCategory(updated.getCategory());
        expense.setDate(updated.getDate());
        expense.setDescription(updated.getDescription());

        return repository.save(expense);
    }

    public void deleteExpense(Long id)
    {
        Expense expense = getExpenseById(id);
        repository.delete(expense);
    }

    public Map<String, Object>
    getMonthlySummary(YearMonth month)
    {

        List<Expense> expenses
            = repository
                  .findByDateGreaterThanEqualAndDateLessThanOrderByDateDesc(
                      month.atDay(1),
                      month.plusMonths(1).atDay(1));

        BigDecimal total = BigDecimal.ZERO;

        Map<String, BigDecimal> byCategory
            = new LinkedHashMap<>();

        for (Expense expense : expenses) {

            total = total.add(expense.getAmount());

            byCategory.merge(expense.getCategory(),
                             expense.getAmount(),
                             BigDecimal::add);
        }

        Map<String, Object> summary = new LinkedHashMap<>();

        summary.put("month", month.toString());
        summary.put("total", total);
        summary.put("byCategory", byCategory);

        return summary;
    }
}

Explanation:

  • getAllExpenses() retrieves expenses in descending date order.
  • getExpenseById() finds a record or returns HTTP 404 when it does not exist.
  • addExpense() stores a new record.
  • updateExpense() modifies an existing expense.
  • deleteExpense() removes a record.
  • getMonthlySummary() calculates the total and category-wise spending for a selected month.

The monthly summary uses BigDecimal to calculate monetary totals and YearMonth to identify the reporting period.

Step 6: Create the REST Controller

The controller exposes REST API endpoints for the React frontend.

Create a controller package and add ExpenseController.java.

File: ExpenseController.java

Java
package com.example.expensetracker.controller;

import com.example.expensetracker.entity.Expense;
import com.example.expensetracker.service.ExpenseService;
import jakarta.validation.Valid;
import java.time.YearMonth;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

@RestController
@RequestMapping("/api/expenses")
@CrossOrigin(origins = "http://localhost:5173")
public class ExpenseController {

    private final ExpenseService service;

    public ExpenseController(ExpenseService service)
    {
        this.service = service;
    }

    @GetMapping public List<Expense> getAllExpenses()
    {
        return service.getAllExpenses();
    }

    @GetMapping("/summary")
    public Map<String, Object>
    getMonthlySummary(@RequestParam String month)
    {

        try {
            return service.getMonthlySummary(
                YearMonth.parse(month));
        }
        catch (DateTimeParseException ex) {
            throw new ResponseStatusException(
                HttpStatus.BAD_REQUEST,
                "Month must use YYYY-MM format");
        }
    }

    @GetMapping("/{id}")
    public Expense getExpense(@PathVariable Long id)
    {
        return service.getExpenseById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Expense
    addExpense(@Valid @RequestBody Expense expense)
    {

        return service.addExpense(expense);
    }

    @PutMapping("/{id}")
    public Expense
    updateExpense(@PathVariable Long id,
                  @Valid @RequestBody Expense expense)
    {

        return service.updateExpense(id, expense);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteExpense(@PathVariable Long id)
    {
        service.deleteExpense(id);
    }
}

Explanation:

  • @RestController defines the class as a REST API controller.
  • @RequestMapping defines the base endpoint /api/expenses.
  • @GetMapping, @PostMapping, @PutMapping, and @DeleteMapping handle HTTP requests.
  • @Valid enables request body validation.
  • @CrossOrigin permits requests from the local React development server.

Expense Management API Endpoints

Method

API Endpoint

Description

GET

/api/expenses

Retrieve all expenses

GET

/api/expenses/{id}

Retrieve a specific expense

POST

/api/expenses

Add a new expense

PUT

/api/expenses/{id}

Update an expense

DELETE

/api/expenses/{id}

Delete an expense

GET

/api/expenses/summary?month=2026-10

Get the monthly spending summary

Step 7: Run and Test the Spring Boot Backend

Open a terminal in the backend project directory and execute:

mvn spring-boot:run

Once the application starts, the backend will be available at:

http://localhost:8080

Open Postman and create the following request.

Method: POST

URL:

http://localhost:8080/api/expenses

Request Body (JSON):

{
  "amount": 450.00,
  "category": "Food",
  "date": "2026-10-12",
  "description": "Lunch"
}

Expected Response: HTTP 201 Created

{
  "id": 1,
  "amount": 450.00,
  "category": "Food",
  "date": "2026-10-12",
  "description": "Lunch"
}

Expected Result: The API returns a JSON array containing the saved expense records.

Frontend Development using React

With the Spring Boot backend ready, we will now build the React frontend. It will provide an expense entry form, a list of recorded expenses, filters, and a monthly summary.

Step 8: Create the React Project

Open a new terminal and execute:

npm create vite@latest expense-frontend -- --template react

Navigate to the project directory:

cd expense-frontend

Install the dependencies:

npm install
npm install axios

Here, Vite provides the React development environment, while Axios sends HTTP requests to the backend.

Screenshot-2026-10-13-121632

Step 9: Create the Axios API Service

Create an api folder inside src and add expenseApi.js.

This service will manage communication between React and Spring Boot.

File: src/api/expenseApi.js

JavaScript
import axios from "axios";

const API_URL = "http://localhost:8080/api/expenses";

export const getExpenses = () => axios.get(API_URL);

export const addExpense
    = (expense) => axios.post(API_URL, expense);

export const updateExpense = (id, expense) => axios.put(
    `${API_URL}/${id}`, expense);

export const deleteExpense
    = (id) => axios.delete(`${API_URL}/${id}`);

export const getMonthlySummary = (month) => axios.get(
    `${API_URL}/summary`, {params : {month}});

Explanation:

  • getExpenses() retrieves all expense records.
  • addExpense() sends new expense details.
  • updateExpense() modifies an existing record.
  • deleteExpense() removes an expense.
  • getMonthlySummary() retrieves total and category-wise spending for a specified month.

Step 10: Create the Expense Form Component

The Expense Form allows users to enter expense details and edit existing records.

Create ExpenseForm.jsx inside the components folder.

File: src/components/ExpenseForm.jsx

JavaScript
import {useEffect, useState} from "react";

const initialForm = {
    amount : "",
    category : "Food",
    date : new Date().toLocaleDateString("en-CA"),
    description : ""
};

function ExpenseForm({onSave, editingExpense, onCancel})
{
    const [form, setForm] = useState(initialForm);

    useEffect(() => {
        if (editingExpense) {
            setForm({
                amount : String(editingExpense.amount),
                category : editingExpense.category,
                date : editingExpense.date,
                description :
                    editingExpense.description || ""
            });
        }
        else {
            setForm({
                ...initialForm,
                date :
                    new Date().toLocaleDateString("en-CA")
            });
        }
    }, [ editingExpense ]);

    const handleChange = (e) => {
        const {name, value} = e.target;

        setForm(
            (previous) => ({...previous, [name] : value}));
    };

    const handleSubmit = (e) => {
        e.preventDefault();

        if (!form.amount || Number(form.amount) <= 0) {
            return;
        }

        onSave({
            amount : form.amount,
            category : form.category,
            date : form.date,
            description : form.description.trim()
        });
    };

  return (
    <form className="expense-form" onSubmit={handleSubmit}>
      <h2>{editingExpense ? "Edit Expense" : "Add Expense"}</h2>

      <label htmlFor="amount">Amount (₹)</label>
      <input
  id = "amount"
  type = "number"
  name = "amount"
  placeholder = "Enter amount"
  min = "0.01"
  max = "9999999999.99"
  step = "0.01"
        value={form.amount}
        onChange={handleChange}
        required
      />

      <label htmlFor="category">Category</label>
      <select
        id="category"
        name="category"
        value={form.category}
        onChange={handleChange}
      >
        <option value="Food">Food</option>
        <option value="Travel">Travel</option>
        <option value="Shopping">Shopping</option>
        <option value="Bills">Bills</option>
        <option value="Health">Health</option>
        <option value="Others">Others</option>
      </select>

      <label htmlFor="date">Expense Date</label>
      <input
        id="date"
        type="date"
        name="date"
        value={form.date}
        onChange={handleChange}
        required
      />

      <label htmlFor="description">Description</label>
      <textarea
        id="description"
        name="description"
        placeholder="Enter expense details"
        maxLength={255}
        value={form.description}
        onChange={handleChange}
      />

      <div className="actions">
        <button type="submit">
          {editingExpense ? "Update Expense" : "Add Expense"}
        </button>

        {editingExpense && (
          <button
            type="button"
            className="secondary"
            onClick={onCancel}
          >
            Cancel
          </button>
        )
}
      </div>
    </form>
  );
      }

      export default ExpenseForm;

Explanation:

  • useState stores the current form values.
  • useEffect populates the form when the user selects an expense for editing.
  • handleChange() updates the corresponding input field.
  • handleSubmit() passes expense information to the parent component.

The expense amount is sent as a decimal string. Spring Boot converts it to BigDecimal for validation and storage.

Step 11: Create the Expense List Component

The Expense List displays saved records in a table with Edit and Delete buttons.

File: src/components/ExpenseList.jsx

JavaScript
function ExpenseList({expenses, onEdit, onDelete})
{
    if (expenses.length === 0) {
    return <p>No expenses found for the selected filters.</p>;
  }

  return (
    <div className="table-wrapper">
      <table>
        <thead>
          <tr>
            <th>Date</th>
            <th>Category</th>
            <th>Description</th>
            <th>Amount</th>
            <th>Actions</th>
          </tr>
        </thead>

        <tbody>
    {expenses.map((expense) => (
            <tr key={expense.id}>
              <td>{expense.date}</td>
              <td>{expense.category}</td>
              <td>{expense.description || "-"}</td>
              <td>
                ₹{Number(expense.amount).toLocaleString("en-IN", {
                  minimumFractionDigits: 2,
                  maximumFractionDigits: 2
                })}
              </td>

              <td>
                <div className="actions">
                  <button
        className = "secondary"
                    onClick={() => onEdit(expense)}
                  >
                    Edit
                  </button>

                  <button
                    className="danger"
                    onClick={() => onDelete(expense.id)}
                  >
                    Delete
                  </button>
                </div>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
    }

    export default ExpenseList;

Explanation:

  • expenses.map() creates a row for each record.
  • The amount is formatted with the Indian rupee symbol.
  • Edit and Delete buttons trigger functions provided by the parent component.
  • An empty-state message is displayed when no matching expenses exist.

The number conversion here is for display formatting. Monetary calculations remain on the backend using BigDecimal.

Step 12: Create the Monthly Expense Summary

The Monthly Summary component displays the total spending and category-wise breakdown returned by Spring Boot.

File: src/components/ExpenseSummary.jsx

JavaScript
function ExpenseSummary({summary})
{
    if (!summary) {
        return null;
    }

    const formatMoney
        = (amount) => Number(amount).toLocaleString(
            "en-IN",
            {style : "currency", currency : "INR"});

  return (
    <section className="summary">
      <h2>Monthly Expense Summary</h2>

      <div className="total-card">
        <p>Total Expenses</p>
        <h3>{formatMoney(summary.total)}</h3>
      </div>

      <h3>Category-wise Spending</h3>

      {Object.keys(summary.byCategory).length === 0 ? (
        <p>No expenses recorded for this month.</p>
      ) : (
        <ul className="category-list">
          {Object.entries(summary.byCategory).map(
            ([category, amount]) => (
              <li key={category}>
                <span>{category}</span>
                <strong>{formatMoney(amount)}</strong>
              </li>
            )
          )}
        </ul>
      )}
    </section>
  );
}

export default ExpenseSummary;


Explanation:

  • summary.total displays the selected month's total expenses.
  • summary.byCategory contains spending totals grouped by category.
  • Object.entries() converts the category totals into displayable rows.
  • The component displays an empty-state message when no expenses exist for the selected month.

Step 13: Integrate the Components in App.jsx

The main application component connects the expense form, list, summary, and Axios services.

It also manages the selected month and category filters.

File: src/App.jsx

JavaScript
import "./App.css";

import {useEffect, useState} from "react";

import {
    addExpense,
    deleteExpense,
    getExpenses,
    getMonthlySummary,
    updateExpense
} from "./api/expenseApi";
import ExpenseForm from "./components/ExpenseForm";
import ExpenseList from "./components/ExpenseList";
import ExpenseSummary from "./components/ExpenseSummary";

function getCurrentMonth()
{
    const now = new Date();
    const year = now.getFullYear();
    const month
        = String(now.getMonth() + 1).padStart(2, "0");

    return `${year}-${month}`;
}

function App()
{
    const [expenses, setExpenses] = useState([]);
    const [summary, setSummary] = useState(null);
    const [editingExpense, setEditingExpense]
        = useState(null);

    const [month, setMonth] = useState(getCurrentMonth);
    const [category, setCategory] = useState("All");

    const [loading, setLoading] = useState(true);
    const [error, setError] = useState("");

    useEffect(() => {
        let active = true;

        async function loadData()
        {
            setLoading(true);
            setError("");

            try {
                const [expenseResponse, summaryResponse]
                    = await Promise.all([
                          getExpenses(),
                          getMonthlySummary(month)
                      ]);

                if (active) {
                    setExpenses(expenseResponse.data);
                    setSummary(summaryResponse.data);
                }
            }
            catch (err) {
                if (active) {
                    setError("Unable to load expenses.");
                    setSummary(null);
                }
            }
            finally {
                if (active) {
                    setLoading(false);
                }
            }
        }

        loadData();

        return () => { active = false; };
    }, [ month ]);

    const refreshExpenses = async () => {
        const [expenseResponse, summaryResponse]
            = await Promise.all([
                  getExpenses(), getMonthlySummary(month)
              ]);

        setExpenses(expenseResponse.data);
        setSummary(summaryResponse.data);
    };

    const handleSave = async (expense) => {
        try {
            setError("");

            if (editingExpense) {
                await updateExpense(editingExpense.id,
                                    expense);
            }
            else {
                await addExpense(expense);
            }

            setEditingExpense(null);
            await refreshExpenses();
        }
        catch (err) {
            setError("Unable to save expense.");
        }
    };

    const handleDelete = async (id) => {
        if (!window.confirm("Delete this expense?")) {
            return;
        }

        try {
            setError("");
            await deleteExpense(id);

            if (editingExpense?.id === id) {
                setEditingExpense(null);
            }

            await refreshExpenses();
        }
        catch (err) {
            setError("Unable to delete expense.");
        }
    };

    const filteredExpenses = expenses.filter((expense) => {
        const matchesMonth = expense.date.startsWith(month);

        const matchesCategory
            = category === "All"
              || expense.category === category;

        return matchesMonth && matchesCategory;
    });

  return (
    <div className="container">
      <header className="page-header">
        <h1>Personal Expense Tracker</h1>
        <p>Track your expenses and understand your spending</p>
      </header>

      {error && <p className="error">{error}</p>
}

< ExpenseForm
editingExpense = {editingExpense} onSave
    = {handleSave} onCancel
    = {() => setEditingExpense(
           null)} />

      <section className="filters">
        <h2>Filter Expenses</h2
      >

      <div className = "filter-fields"><div>
      <label htmlFor = "month">Select Month<
          /label>
            <input
              id="month"
              type="month"
              value={month}
              onChange={(e) => setMonth(e.target.value)}
            />
      </div>

          <div>
            <label htmlFor="filterCategory">Category</label>< select
id = "filterCategory"
              value={category}
              onChange={(e) => setCategory(e.target.value)}
            >
              <option value="All">All Categories</option>
              <option value="Food">Food</option>
              <option value="Travel">Travel</option>
              <option value="Shopping">Shopping</option>
              <option value="Bills">Bills</option>
              <option value="Health">Health</option>
              <option value="Others">Others</option>
            </select>
          </div>
        </div>
      </section>

      {loading ? (
        <p>Loading expenses...</p>
      ) : (
        <>
          <ExpenseSummary summary={
    summary} />

          <section className="expense-section">
            <h2>Expense Records</h2>

            <ExpenseList
              expenses={filteredExpenses}
              onEdit={setEditingExpense}
              onDelete={
    handleDelete}
            />
          </section>
        </>
      )}
    </div>
  );
              }

              export default App;


Explanation:

  • useState manages the expense list, selected month, category, editing state, and summary.
  • useEffect retrieves expense records and summary data whenever the selected month changes.
  • handleSave() adds or updates expenses.
  • handleDelete() deletes the selected record after confirmation.
  • filteredExpenses shows records matching the selected month and category.
  • refreshExpenses() retrieves updated expenses and monthly totals after a successful operation.

Note: The category filter applies to the expense table. The monthly summary intentionally includes all categories for the selected month, so the monthly total remains consistent.

Step 14: Add CSS Styling

Add the following styles to create a clean expense dashboard.

File: src/App.css

CSS
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: Arial, sans-serif;
  color: #243247;
  background: #f4f6f9;
}

.container {
  max-width: 1000px;
  margin: 35px auto;
  padding: 20px;
}

.page-header {
  text-align: center;
  margin-bottom: 30px;
}

h1 {
  color: #1e3a5f;
}

.expense-form,
.filters,
.summary,
.expense-section {
  background: white;
  border-radius: 10px;
  padding: 22px;
  margin-bottom: 20px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}

input,
select,
textarea {
  width: 100%;
  padding: 11px;
  margin: 8px 0 16px;
  border: 1px solid #cbd5e1;
  border-radius: 6px;
  font: inherit;
}

label {
  display: block;
  font-weight: 600;
}

button {
  padding: 10px 15px;
  border: none;
  border-radius: 6px;
  color: white;
  background: #2563eb;
  cursor: pointer;
}

button:hover {
  opacity: 0.9;
}

.actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.secondary {
  background: #64748b;
}

.danger {
  background: #dc2626;
}

.filter-fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px;
}

.total-card {
  background: #e0f2fe;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 20px;
}

.total-card h3 {
  font-size: 28px;
  margin: 8px 0;
  color: #075985;
}

.category-list {
  list-style: none;
  padding: 0;
}

.category-list li {
  display: flex;
  justify-content: space-between;
  padding: 12px 0;
  border-bottom: 1px solid #e2e8f0;
}

.table-wrapper {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  min-width: 620px;
}

th,
td {
  padding: 12px;
  border-bottom: 1px solid #e2e8f0;
  text-align: left;
}

th {
  background: #f1f5f9;
}

.error {
  padding: 12px;
  background: #fee2e2;
  color: #b91c1c;
  border-radius: 6px;
}

@media (max-width: 600px) {
  .filter-fields {
    grid-template-columns: 1fr;
  }

  .container {
    padding: 12px;
    margin: 12px auto;
  }
}

Step 15: Configure the React Entry Point

Replace the generated src/main.jsx with:

File: src/main.jsx

JavaScript
import React from "react";
import ReactDOM from "react-dom/client";

import App from "./App.jsx";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Step 16: Run the React Application

Open the terminal in the React project directory and execute:

npm run dev

The Vite development server normally starts at:

Open the address in your browser.

Ensure both applications are running:

Application

Address

React Frontend

http://localhost:5173

Spring Boot Backend

http://localhost:8080

Expense API

http://localhost:8080/api/expenses

Screenshot-2026-10-13-122129
Starting the Personal Expense Tracker frontend using Vite.

Step 17: Test the Personal Expense Tracker

Once the frontend and backend are running, test each feature to verify that the application works correctly.

1. Add a New Expense

Enter the following details in the expense form:

Field

Value

Amount

₹450.00

Category

Food

Date

2026-10-12

Description

Lunch

Click Add Expense .

Expected Result: The expense is saved in the MySQL database and appears in the expense table when October 2026 is selected.

Screenshot-2026-10-13-120340

2. View All Expenses

Add a few more sample records.

Amount

Category

Date

Description

₹450.00

Food

2026-10-12

Lunch

₹200.00

Travel

2026-10-12

Cab Fare

₹1,500.00

Shopping

2026-10-13

Clothes

₹800.00

Bills

2026-10-13

Internet Bill

₹350.00

Food

2026-10-13

Dinner

Expected Result: The expense table displays the saved records with their dates, categories, descriptions, and amounts.

Screenshot-2026-10-13-122916

3. Edit an Expense

Click Edit beside an expense, change its amount or description, and click Update Expense.

Expected Result: The updated record appears in the expense table and the database is updated.

Screenshot-2026-10-13-123022

4. Delete an Expense

Click Delete beside an expense and confirm the action.

Expected Result: The expense is removed from the database, and the list and summary are refreshed.

Screenshot-2026-10-13-123253

5. Filter Expenses

Select 2026-10 in the month filter and Food in the category filter.

Expected Result: Only Food expenses recorded in October 2026 appear in the expense table.

Screenshot-2026-10-13-120411

6. Check Monthly Expense Summary

Using the five sample records above, before any edits or deletions, the monthly total should be:

Screenshot-2026-10-13-122447
Personal Expense Tracker dashboard displaying recorded expenses and the monthly summary.

Step 18: Verify the Data in MySQL

Open MySQL Workbench and execute the following query:

USE expensedb;
SELECT * FROM expenses;

Expected Result: The expenses table contains the expense records created through the React application.

You can also verify the monthly expense total using:

SELECT SUM(amount) AS total_expenses
FROM expenses
WHERE date >= '2026-10-01'
  AND date < '2026-11-01';
Screenshot-2026-10-13-121518
Verifying expense records in the MySQL database.

Common Errors and Troubleshooting

Error

Possible Cause

Solution

CORS policy error

React origin is not allowed

Verify @CrossOrigin and the frontend port

MySQL access denied

Incorrect database credentials

Verify MySQL username, password, and permissions

Connection refused on port 8080

Spring Boot is not running

Start the backend and inspect its console

HTTP 400 Bad Request

Invalid amount, date, or category

Verify the submitted JSON and validation rules

HTTP 404 Not Found

Incorrect endpoint or missing expense ID

Check the URL and requested ID

Blank React page

JavaScript or import error

Inspect browser console and terminal output

Expenses not displayed

API request failed or selected filters exclude the records

Check the Network tab and filter values

npm.ps1 execution error

PowerShell execution policy restriction

Use npm.cmd in PowerShell

Date formatting note: To make the form's default date consistent across browsers, use the following helper instead of relying on locale-based date formatting:

JavaScript
function getToday()
{
    const now = new Date();
    const year = now.getFullYear();
    const month
        = String(now.getMonth() + 1).padStart(2, "0");
    const day = String(now.getDate()).padStart(2, "0");

    return `${year}-${month}-${day}`;
}

In ExpenseForm.jsx, replace both occurrences of new Date().toLocaleDateString("en-CA") with getToday(). This produces the YYYY-MM-DD format required by the date input.

Future Enhancements

After completing the basic project, the application can be extended with additional capabilities.

  • User Authentication: Implement Spring Security and JWT to allow users to manage their own private expense records.
  • Budget Management: Let users set monthly spending limits and compare actual expenses against their budgets.
  • Expense Charts: Display spending trends and category distributions using charts.
  • Search and Pagination: Add backend-supported searching, filtering, and pagination for large datasets.
  • Export Reports: Allow users to download expense records as CSV or PDF files.
  • Recurring Expenses: Automatically create scheduled expense records for subscriptions or regular payments.

For a production application, also add authentication and authorization, DTO-based API contracts, database migrations, centralized error handling, and automated tests. The current project is a single-user educational example and does not separate financial records by user.

Comment

Explore