Skip to content

Latest commit

 

History

History
850 lines (628 loc) · 31.4 KB

File metadata and controls

850 lines (628 loc) · 31.4 KB

Expense Tracker — Complete Code Breakdown

Every file in this project explained line by line.

Table of Contents

  • 1. How All Files Work Together — The Big Picture
  • 2. server.js — The Backend Server (Brain of the App)
  • 3. database.sql — Database Setup Script
  • 4. package.json — Project Configuration
  • 5. package-lock.json — Dependency Lock File
  • 6. .env.example — Environment Variables Template
  • 7. .gitignore — Git Exclusions
  • 8. public/index.html — The Web Page
  • 9. public/script.js — Frontend Logic
  • 10. public/style.css — Visual Styling
  • 11. README.md — Project Documentation

1. How All Files Work Together — The Big Picture

Before diving into individual files, here's how they all connect:

USER'S BROWSER                              EC2 SERVER
┌──────────────────┐
┌──────────────────────────────────┐
│                  │                        │
│  index.html      │  ── loads ──►          │  server.js
│  (the page)      │                        │  (Express web server)
│       │          │                        │       │
│       │ uses     │                        │       │ serves files from
│       ▼          │                        │       │ public/ folder
│  style.css       │                        │       │
│  (appearance)    │                        │       │ provides API:
│       │          │                        │       │ GET  /api/expenses
│       │ loads    │                        │       │ POST /api/expenses
│       ▼          │                        │       │ DELETE /api/expenses/:id
│  script.js ──────── API calls ──────────► │       │ DELETE /api/expenses
│  (browser logic) │                        │       │
│       │          │                        │       │ talks to
│       │ calls    │                        │       ▼
│       │ fetch()  │                        │  PostgreSQL Database
│       │          │                        │  (database.sql created this)
│       ▼          │                        │       │
│  Shows results   │ ◄── JSON response ──── │  Returns data
│  in the table    │                        │
└──────────────────┘
└──────────────────────────────────┘

CONFIGURATION FILES (not running code):
┌───────────────────────────────────────────────────────────────┐
│  package.json      → Tells npm what to install                │
│  package-lock.json → Locks exact dependency versions          │
│  .env.example      → Template for secret configuration        │
│  .gitignore        → Tells Git what NOT to track              │
│  README.md         → Documentation for humans                 │
└───────────────────────────────────────────────────────────────┘

The Request-Response Cycle

Every action in the app follows this cycle:

  1. User interacts with index.html (clicks button, submits form)
  2. script.js captures the event and makes an API call (fetch)
  3. The HTTP request travels over the internet to EC2
  4. server.js receives the request and routes it
  5. server.js talks to PostgreSQL (SQL query)
  6. PostgreSQL returns data
  7. server.js sends a JSON response back to the browser
  8. script.js receives the JSON and updates the HTML page

2. server.js — The Backend Server

This is the most important file in the project — the entire backend in a single file. It runs on the EC2 server using Node.js.

Full Code:

// Simple Expense Tracker Server
// All code in one file for beginners!

const express = require("express");
const { Pool } = require("pg");
const path = require("path");

const app = express();
const PORT = 3000;

// ========== DATABASE SETUP ==========
// Change these values to match your PostgreSQL setup
const pool = new Pool({
  user: "postgres", // Your PostgreSQL username
  host: "localhost", // Database location
  database: "expense_tracker", // Database name
  password: "Obaid@pst123", // Your PostgreSQL password (CHANGE THIS!)
  port: 5432, // PostgreSQL port
});

// Test database connection
pool.connect((err) => {
  if (err) {
    console.log("❌ Database connection failed!");
    console.log("Error:", err.message);
  } else {
    console.log("✅ Database connected successfully!");
  }
});

// ========== MIDDLEWARE ==========
app.use(express.json()); // Parse JSON data
app.use(express.static(path.join(__dirname, "public"))); // Serve HTML/CSS/JS files

// ========== ROUTES ==========

// Home page
app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

// Get all expenses
app.get("/api/expenses", async (req, res) => {
  try {
    const result = await pool.query(
      "SELECT * FROM expenses ORDER BY date DESC"
    );
    res.json(result.rows);
  } catch (err) {
    console.log("Error:", err);
    res.status(500).json({ error: "Failed to fetch expenses" });
  }
});

// Add new expense
app.post("/api/expenses", async (req, res) => {
  const { description, amount, category, date } = req.body;

  // Check if all fields are filled
  if (!description || !amount || !category || !date) {
    return res.status(400).json({ error: "Please fill all fields" });
  }

  try {
    const result = await pool.query(
      "INSERT INTO expenses (description, amount, category, date) VALUES ($1, $2, $3, $4) RETURNING *",
      [description, amount, category, date]
    );
    res.json(result.rows[0]);
  } catch (err) {
    console.log("Error:", err);
    res.status(500).json({ error: "Failed to add expense" });
  }
});

// Delete single expense by ID
app.delete("/api/expenses/:id", async (req, res) => {
  const { id } = req.params;

  try {
    const result = await pool.query(
      "DELETE FROM expenses WHERE id = $1 RETURNING *",
      [id]
    );

    if (result.rows.length === 0) {
      return res.status(404).json({ error: "Expense not found" });
    }

    res.json({ message: "Expense deleted successfully" });
  } catch (err) {
    console.log("Error:", err);
    res.status(500).json({ error: "Failed to delete expense" });
  }
});

// Delete ALL expenses
app.delete("/api/expenses", async (req, res) => {
  try {
    await pool.query("DELETE FROM expenses");
    res.json({ message: "All expenses deleted successfully" });
  } catch (err) {
    console.log("Error:", err);
    res.status(500).json({ error: "Failed to delete all expenses" });
  }
});

// ========== START SERVER ==========
app.listen(PORT, () => {
  console.log("=================================");
  console.log(`🚀 Server running on http://localhost:${PORT}`);
  console.log("=================================");
});

Line-by-Line Explanation:

Lines 1-2: Comments

// Simple Expense Tracker Server
// All code in one file for beginners!

Lines starting with // are comments — they're ignored by Node.js and exist only for human readers. Good practice to describe what a file does at the top.

Line 4:

require("express")

Node.js's way of importing a module (library). It loads the Express.js framework from the node_modules folder (installed by npm install).

const express = require("express");

const express — Stores the imported module in a constant variable. What is Express? A web framework that makes it easy to create an HTTP server, define URL routes, and handle requests/responses. Without Express, you'd need ~50 lines of boilerplate code just to create a basic server.

Line 5:

require("pg")

Imports the pg (node-postgres) library that allows Node.js to communicate with PostgreSQL.

const { Pool } = require("pg");

This is destructuring. The pg module exports multiple things; we only need the Pool class. It's equivalent to writing:

const pg = require("pg");
const Pool = pg.Pool;

What is a Pool? A connection pool manages multiple database connections. Instead of opening a new connection for every query (slow: ~20-50ms per connection) and closing it after, the pool keeps connections alive and reuses them.

Line 6:

require("path")

Imports Node.js's built-in path module. This is NOT installed by npm — it comes with Node.js itself.

What does it do? Helps construct file paths that work on any operating system (Windows uses \, Mac/Linux uses /). The path.join() method builds paths correctly regardless of OS.

Line 8:

const app = express();

Calling Express as a function creates a new application instance. This object IS the web server. All routes, middleware, and configuration are added to this object. Think of app as the central hub — everything flows through it.

Line 9:

const PORT = 3000;

Defines which port the server listens on. What is a port? A number that identifies a specific service on a machine. Like apartment numbers in a building — the building (server) has one address (IP), but each apartment (service) has its own number. Port 3000 is a common development port. Port 80 is the default HTTP port (no :80 needed in URLs). Port 443 is HTTPS.

Lines 13-19: Database Connection Pool

const pool = new Pool({
  user: "postgres",
  host: "localhost",
  database: "expense_tracker",
  password: "Obaid@pst123",
  port: 5432,
});

new Pool({...}) — Creates a new connection pool with the specified configuration. Configuration options:

Option Value Meaning
user "postgres" PostgreSQL username (default superuser)
host "localhost" Where the database is running. localhost means the same machine as the server
database "expense_tracker" The specific database name to connect to
password "Obaid@pst123" The PostgreSQL user's password
port 5432 PostgreSQL's default port number

Important security note: The password is hardcoded here, which is NOT a best practice. In production, you'd use environment variables (the .env.example file shows how).

Lines 22-29: Test Database Connection

pool.connect((err) => {
  if (err) {
    console.log("❌ Database connection failed!");
    console.log("Error:", err.message);
  } else {
    console.log("✅ Database connected successfully!");
  }
});

pool.connect(callback) — Attempts to get a connection from the pool. This is a one-time test to verify the database is reachable when the server starts.

(err) => { ... } — A callback function. Node.js is asynchronous — instead of waiting for the connection to complete, it provides a callback that runs when the result is ready.

if (err) — If err exists (is not null), the connection failed. The error object contains details about what went wrong.

console.log(...) — Prints messages to the terminal (visible in the EC2 SSH session or PM2 logs). These messages help you know if the database is connected or not when the server starts.

Line 32:

app.use(express.json());

app.use(...) — Registers middleware. Middleware is a function that processes every incoming request BEFORE it reaches a route handler.

express.json() — Built-in middleware that parses JSON request bodies. When the browser sends POST /api/expenses with a JSON body like {"description":"Groceries","amount":1200}, this middleware:

  1. Reads the raw request body (a string of text)
  2. Parses it as JSON
  3. Makes it available as req.body (a JavaScript object)

Without this middleware, req.body would be undefined and you couldn't read the form data.

Line 33:

app.use(express.static(path.join(__dirname, "public")));

express.static(directory) — Built-in middleware that serves static files (HTML, CSS, JS, images) from a folder.

path.join(__dirname, "public") — Constructs the absolute path to the public folder:

  • __dirname — A Node.js global variable that contains the absolute path of the directory where server.js is located (e.g., /home/ubuntu/expense-tracker)
  • path.join(...) — Joins path segments with the correct separator

Result: /home/ubuntu/expense-tracker/public

What this does: When a browser requests http://13.201.76.0:3000/style.css, Express automatically looks for public/style.css and serves it. Same for script.js, index.html, etc. This is why the browser can load the HTML page, CSS styles, and JavaScript without any explicit route for each file.

Lines 38-40: Home Page Route

app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

app.get("/", handler) — Registers a route handler for GET / (the root URL).

"/" — The URL path. When someone visits http://13.201.76.0:3000/, the path is /.

(req, res) => { ... } — The handler function:

  • reqRequest object — contains info about the incoming request (headers, URL, query params, body)
  • resResponse object — used to send data back to the browser

res.sendFile(...) — Sends a file as the response. The browser receives the HTML file and renders it.

Lines 43-53: GET all expenses

app.get("/api/expenses", async (req, res) => {
  try {
    const result = await pool.query(
      "SELECT * FROM expenses ORDER BY date DESC"
    );
    res.json(result.rows);
  } catch (err) {
    console.log("Error:", err);
    res.status(500).json({ error: "Failed to fetch expenses" });
  }
});

app.get("/api/expenses", handler) — Handles GET /api/expenses. The frontend calls this to load all expenses.

async (req, res) => { ... }async keyword enables await inside the function. Database queries take time; await pauses execution until the query completes.

try { ... } catch (err) { ... }Error handling. If anything inside try throws an error, execution jumps to catch instead of crashing the server.

Inside the try block:

  • pool.query("SELECT * FROM expenses ORDER BY date DESC") — Sends a SQL query to PostgreSQL
  • await — Waits for PostgreSQL to execute the query and return results
  • result — The query result object, which contains result.rows (an array of expense objects)
  • res.json(result.rows) — Sends the rows array as a JSON response to the browser. Express automatically converts the JavaScript array to a JSON string, sets the Content-Type header to application/json, and sends the response.

Lines 56-74: POST Add New Expense

app.post("/api/expenses", async (req, res) => {
  const { description, amount, category, date } = req.body;

  // Check if all fields are filled
  if (!description || !amount || !category || !date) {
    return res.status(400).json({ error: "Please fill all fields" });
  }

  try {
    const result = await pool.query(
      "INSERT INTO expenses (description, amount, category, date) VALUES ($1, $2, $3, $4) RETURNING *",
      [description, amount, category, date]
    );
    res.json(result.rows[0]);
  } catch (err) {
    console.log("Error:", err);
    res.status(500).json({ error: "Failed to add expense" });
  }
});

app.post("/api/expenses", handler) — Handles POST /api/expenses. The frontend calls this when the user submits the "Add Expense" form.

Destructuring the request body: When the browser sends POST /api/expenses with a JSON body like {"description":"Groceries","amount":1200,"category":"Food","date":"2025-11-01"}, this line extracts each value:

const { description, amount, category, date } = req.body;

Result:

  • description = "Groceries"
  • amount = 1200
  • category = "Food"
  • date = "2025-11-01"

Validation:

if (!description || !amount || !category || !date) {
  return res.status(400).json({ error: "Please fill all fields" });
}

!description — The ! (NOT) operator. If description is undefined, null, "" (empty), or 0, then !description is true.

|| — OR operator. If ANY of the four fields is missing, the condition is true.

return res.status(400).json(...) — Sends a 400 (Bad Request) error and stops the function (the return keyword exits the function immediately so the code below doesn't run).

This is server-side validation — even if someone bypasses the browser form, the server still checks the data. Never trust client-side validation alone.

Database Insert:

const result = await pool.query(
  "INSERT INTO expenses (description, amount, category, date) VALUES ($1, $2, $3, $4) RETURNING *",
  [description, amount, category, date]
);
  • INSERT INTO expenses (description, amount, category, date) — SQL command to add a new row to the expenses table, specifying which columns to fill
  • VALUES ($1, $2, $3, $4)Parameterized query (prepared statement). The $1, $2, $3, $4 are placeholders that get replaced with actual values
  • [description, amount, category, date] — The second argument is an array of values that map to the placeholders: $1description, $2amount, $3category, $4date

Why parameterized queries? Security! Prevents SQL injection (see Q12 above).

  • RETURNING * — PostgreSQL-specific feature. After inserting the row, it returns the complete newly-created row (including the auto-generated id and created_at)
  • result.rows[0] — The first (and only) row returned by RETURNING * — the newly created expense
  • res.json(result.rows[0]) — Sends this back to the browser as JSON. The browser can then use this to confirm the expense was added.

Lines 77-95: DELETE Single Expense

app.delete("/api/expenses/:id", async (req, res) => {
  const { id } = req.params;

  try {
    const result = await pool.query(
      "DELETE FROM expenses WHERE id = $1 RETURNING *",
      [id]
    );

    if (result.rows.length === 0) {
      return res.status(404).json({ error: "Expense not found" });
    }

    res.json({ message: "Expense deleted successfully" });
  } catch (err) {
    console.log("Error:", err);
    res.status(500).json({ error: "Failed to delete expense" });
  }
});

app.delete("/api/expenses/:id", handler) — Handles DELETE /api/expenses/5 (or any ID).

":id" — A route parameter. The colon makes it a variable. When a request comes for /api/expenses/5, Express captures 5 and stores it in req.params.id.

const { id } = req.params; — Destructures the route parameter. id = "5".

"DELETE FROM expenses WHERE id = $1 RETURNING *" — SQL command:

  • DELETE FROM expenses — Delete a row from the expenses table
  • WHERE id = $1 — Only the row where id matches the parameter
  • RETURNING * — Return the deleted row (so we can check if anything was actually deleted)

if (result.rows.length === 0) — If RETURNING * returned no rows, it means no row with that ID existed. We return a 404 (Not Found).

res.json({ message: "Expense deleted successfully" }) — If deletion worked, send a success message.

Lines 98-106: DELETE ALL Expenses

app.delete("/api/expenses", async (req, res) => {
  try {
    await pool.query("DELETE FROM expenses");
    res.json({ message: "All expenses deleted successfully" });
  } catch (err) {
    console.log("Error:", err);
    res.status(500).json({ error: "Failed to delete all expenses" });
  }
});

app.delete("/api/expenses", handler) — Handles DELETE /api/expenses (no ID = delete ALL).

"DELETE FROM expenses" — SQL to delete every row in the table. No WHERE clause = all rows.

No RETURNING * needed because we don't care about the deleted data.

Lines 109-113: Start the Server

app.listen(PORT, () => {
  console.log("=================================");
  console.log(`🚀 Server running on http://localhost:${PORT}`);
  console.log("=================================");
});

app.listen(PORT, callback) — Tells Express to start the HTTP server and listen for connections on port 3000.

The callback runs once the server is ready — it logs a confirmation message.

After this line, the server is running. It will wait indefinitely for HTTP requests and respond to them. PM2 keeps this process alive on EC2.


3. database.sql — Database Setup Script

Full Code:

-- Step 1: Create database
CREATE DATABASE expense_tracker;

-- Step 2: Connect to database (in psql, run: \c expense_tracker)

-- Step 3: Create expenses table
CREATE TABLE expenses (
    id SERIAL PRIMARY KEY,
    description VARCHAR(255) NOT NULL,
    amount DECIMAL(10, 2) NOT NULL,
    category VARCHAR(100) NOT NULL,
    date DATE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Step 4: Add some sample data (optional)
INSERT INTO expenses (description, amount, category, date) VALUES
('Grocery shopping', 1200.00, 'Food', '2025-11-01'),
('Auto rickshaw', 80.00, 'Transportation', '2025-11-02'),
('Movie tickets', 500.00, 'Entertainment', '2025-11-05'),
('Electricity bill', 2500.00, 'Bills', '2025-11-06'),
('Chai and samosa', 50.00, 'Food', '2025-11-07');

Line-by-Line Explanation:

-- is a SQL comment. Everything after on that line is ignored.

CREATE DATABASE — SQL command to create a new database.

expense_tracker — The name of the database. This matches database: "expense_tracker" in server.js.

; — SQL statements end with a semicolon.

A database is a container that holds tables. One PostgreSQL server can have many databases.

CREATE TABLE expenses — Creates a new table named expenses inside the current database.

A table is like a spreadsheet with defined columns. Each row is one expense record.

Column Definitions:

Column Type Purpose
id SERIAL PRIMARY KEY Auto-incrementing integer Unique identifier for each expense. PRIMARY KEY = no two rows can have the same id.
description VARCHAR(255) NOT NULL Variable-length text up to 255 characters What the expense was for. NOT NULL = required.
amount DECIMAL(10, 2) NOT NULL Number with 10 total digits, 2 decimal places Expense amount in ₹. Perfect for money. NOT NULL = required.
category VARCHAR(100) NOT NULL Text up to 100 characters Expense category (Food, Bills, etc.). NOT NULL = required.
date DATE NOT NULL Date (year-month-day format) When the expense occurred. NOT NULL = required.
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP Date + time When the record was created. Auto-set by PostgreSQL.

INSERT INTO expenses (description, amount, category, date) — Insert new rows, specifying which columns to fill.

VALUES (...) — The actual data for each row.

Multiple rows are separated by commas. These are sample/test data — they populate the table so the app has something to display immediately after setup. This step is optional.


4. package.json — Project Configuration

Full Code:

{
  "name": "expense-tracker",
  "version": "1.0.0",
  "description": "Simple expense tracker for beginners",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "keywords": [
    "expense",
    "tracker"
  ],
  "author": "ObaidAbdullah16",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2",
    "pg": "^8.11.0"
  }
}

Field-by-Field Explanation:

Field Value Purpose
"name" "expense-tracker" Project identifier. Must be lowercase, no spaces.
"version" "1.0.0" Semantic versioning: MAJOR.MINOR.PATCH. First stable release.
"description" "Simple expense tracker for beginners" Brief text about the project.
"main" "server.js" The entry point file. When the project is loaded, Node.js starts here.
"scripts"."start" "node server.js" Running npm start in the terminal executes node server.js. This is the command that starts the server.
"keywords" ["expense", "tracker"] Search terms if published to npm.
"author" "ObaidAbdullah16" Who created the project.
"license" "ISC" ISC license — a permissive open-source license similar to MIT.
"dependencies" {express, pg} Packages this project needs to run. When you run npm install, npm reads this and downloads them.

Dependency Version Syntax:

  • "express": "^4.18.2" — The ^ (caret) means: install version 4.18.2 or any compatible higher version (4.18.3, 4.19.0) but NOT 5.0.0 (major version change)
  • "pg": "^8.11.0" — Install 8.11.0 or compatible higher (8.11.1, 8.12.0) but NOT 9.0.0

5. package-lock.json — Dependency Lock File

What It Is

Auto-generated by npm when you run npm install. Never edit manually.

What It Does

While package.json says "I need Express ~4.18.2", package-lock.json says "I need Express exactly 4.18.2, AND its sub-dependency accepts at exactly 1.3.8, AND accepts's sub-dependency mime-types at exactly 2.1.35..."

Why It Matters

Without lock file With lock file
npm install might install different sub-versions on different machines npm install installs identical versions everywhere
App might behave differently on EC2 vs your laptop Guaranteed same behavior
Debugging version-related bugs is hard Reproducible builds

6. .env.example — Environment Variables Template

Full Code:

# Database Configuration
DB_USER=postgres
DB_HOST=localhost
DB_NAME=expense_tracker
DB_PASSWORD=yourpassword
DB_PORT=5432

# Server Configuration
PORT=3000
NODE_ENV=development

# For production, you might want to add:
# DB_HOST=your-rds-endpoint.amazonaws.com (if using AWS RDS)

What It Is

A template file showing what environment variables the project needs. It does NOT contain real values — it shows the structure so anyone setting up the project knows what to configure.

How It's Meant to Be Used

  1. Copy .env.example to .env: cp .env.example .env
  2. Edit .env with your real values: DB_PASSWORD=MyRealPassword123
  3. The .env file is listed in .gitignore so it never gets committed
  4. The server reads from .env using a library like dotenv

Why This Pattern Exists

Problem Solution
Passwords in code get committed to GitHub (public!) Store secrets in .env (never committed)
Different environments need different config Each environment has its own .env
New developers don't know what config is needed .env.example documents all required variables

Current Project Note

Currently, server.js hardcodes the password directly instead of reading from .env. The .env.example exists as documentation for the best-practice approach.


7. .gitignore — Git Exclusions

Full Code:

# Ignore node_modules folder
node_modules/

# Ignore environment files (if you create .env later)
.env

# Ignore logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Ignore OS files
.DS_Store
Thumbs.db

# Ignore editor files
.vscode/
.idea/
*.swp
*.swo
*~

Line-by-Line Explanation:

Entry What It Excludes Why
node_modules/ All npm packages (thousands of files, ~50MB+) Recreated by npm install. Bloats the repo.
.env Environment variables file with real secrets Contains passwords. MUST never be in a public repo.
npm-debug.log* npm error logs Generated files, not source code. The * is a wildcard matching any suffix.
yarn-debug.log* Yarn package manager logs Same as above, for Yarn users.
yarn-error.log* Yarn error logs Same.
.DS_Store macOS hidden folder metadata file Auto-generated by Finder. Not project-related.
Thumbs.db Windows thumbnail cache Auto-generated by Windows Explorer. Not project-related.
.vscode/ VS Code editor settings Personal to each developer — shouldn't be shared.
.idea/ JetBrains IDE settings (WebStorm, IntelliJ) Same — personal editor preferences.
*.swp, *.swo Vim editor swap files Temporary files created while editing in Vim.
*~ Backup files created by some editors Emacs and others create filename~ backups.

8. public/index.html — The Web Page

This is what the user sees. The browser loads this file and renders it as a visual page. (See full file in repository for complete HTML structure.)

Key Sections:

Head Section:

  • Meta tags for character encoding and responsiveness
  • Bootstrap CSS link from CDN
  • Custom CSS link (style.css)

Body Section:

  • Title: "💰 Expense Tracker"
  • Add Expense Form with inputs for:
    • Description (text)
    • Amount (number with step="0.01")
    • Category (select dropdown)
    • Date (date picker)
  • Total Expenses Display (updated dynamically)
  • Expenses Table with:
    • Thead: Column headers (Date, Description, Category, Amount, Action)
    • Tbody: Dynamically populated by script.js

Scripts:

  • Bootstrap JS (at bottom)
  • script.js (at bottom)

Why scripts at bottom? By the time JavaScript runs, all HTML has been parsed and rendered. The JavaScript can safely access all elements by their IDs.


9. public/script.js — Frontend Logic

This runs in the browser (not on the server). It's the bridge between the visible HTML page and the server API. (See full file in repository for complete JavaScript code.)

Key Functions:

loadExpenses() — Fetches all expenses from /api/expenses and rebuilds the table

  • Calls GET /api/expenses
  • Handles empty state
  • Loops through expenses, creates table rows
  • Applies category color coding
  • Calculates and displays total with Indian number formatting

Form Submit Handler — When user submits the form

  • Prevents default form behavior
  • Collects form values
  • Sends POST request to /api/expenses
  • On success: resets form, reloads table, shows alert
  • On error: shows error alert

deleteExpense(id, description) — Delete a single expense

  • Shows confirmation dialog
  • Sends DELETE request to /api/expenses/:id
  • On success: reloads table, shows alert

Delete All Handler — Delete all expenses

  • Shows confirmation dialog ("⚠️ Delete ALL expenses?")
  • Sends DELETE request to /api/expenses
  • On success: reloads table, shows alert

10. public/style.css — Visual Styling

Contains custom CSS that makes the app look polished. (See full file in repository for complete styles.)

Key Styles:

  • Body: Purple gradient background (linear-gradient 135deg from #667eea to #764ba2)
  • H1: White text with text-shadow
  • Cards: Rounded corners (15px), box-shadow, no border
  • Buttons: Primary (purple), hover effect (darker purple)
  • Table: Dark header with matching purple
  • Badges: Color-coded by category (green, red, blue, etc.)

11. README.md — Project Documentation

The quick-start guide visible on the GitHub repository page. Contains project description, features, tech stack, setup instructions, deployment guide, and troubleshooting.