Tier 1 — Beginner

Budget Tracker

Track income and expenses by category — running balance, monthly summary, Intl.NumberFormat for currency.

🐍 Python 🟨 JavaScript

1. The Problem

Build a budget tracker: add income and expense transactions with category and date. Show a running balance, totals by category, and a monthly summary. Persist to JSON.

Why this project? Reduce for financial aggregation, groupBy patterns using an object accumulator, Intl for currency display, date filtering with ISO string comparison — the building blocks of every fintech application.

2. How to Think About This

Each transaction is { id, type ("income"|"expense"), amount, category, date, note }. Balance = sum of incomes − sum of expenses. Category breakdown via reduce into an object. Monthly grouping by slicing the ISO date to YYYY-MM.

3. The Build

budget.mjs
import { readFileSync, writeFileSync, existsSync } from "fs";

const DATA = "budget.json";
const fmt  = amount =>
  new Intl.NumberFormat("en-IN", { style:"currency", currency:"INR" }).format(amount);

export class BudgetTracker {
  #txns;
  constructor(txns = []) { this.#txns = txns; }

  add({ type, amount, category, note = "", date = new Date().toISOString().slice(0,10) }) {
    if (!["income","expense"].includes(type)) throw new Error("type must be income or expense");
    const amt = parseFloat(amount);
    if (isNaN(amt) || amt <= 0) throw new Error("Amount must be a positive number");
    const txn = { id: Date.now(), type, amount: +amt.toFixed(2), category, note, date };
    this.#txns.push(txn);
    return txn;
  }

  balance() {
    return +this.#txns.reduce((sum, t) =>
      t.type === "income" ? sum + t.amount : sum - t.amount, 0).toFixed(2);
  }

  byCategory() {
    return this.#txns.reduce((acc, t) => {
      const key = `${t.type}:${t.category}`;
      acc[key] = +(( acc[key] || 0) + t.amount).toFixed(2);
      return acc;
    }, {});
  }

  byMonth(month) { // "YYYY-MM"
    return this.#txns.filter(t => t.date.startsWith(month));
  }

  summary() {
    const income  = +this.#txns.filter(t=>t.type==="income").reduce((s,t)=>s+t.amount,0).toFixed(2);
    const expense = +this.#txns.filter(t=>t.type==="expense").reduce((s,t)=>s+t.amount,0).toFixed(2);
    return { income, expense, balance: +(income - expense).toFixed(2) };
  }

  all() { return [...this.#txns]; }
  toJSON() { return JSON.stringify(this.#txns, null, 2); }
}

function load() {
  if (!existsSync(DATA)) return new BudgetTracker();
  try { return new BudgetTracker(JSON.parse(readFileSync(DATA,"utf-8"))); } catch { return new BudgetTracker(); }
}
function save(b) { writeFileSync(DATA, b.toJSON()); }

const [,, cmd, ...args] = process.argv;
const budget = load();

switch (cmd) {
  case "income":
  case "expense": {
    const [amount, category="General", ...noteParts] = args;
    if (!amount) { console.error(`Usage: node budget.mjs ${cmd} <amount> [category] [note]`); break; }
    try {
      const t = budget.add({ type: cmd, amount, category, note: noteParts.join(" ") });
      save(budget);
      console.log(`${cmd === "income" ? "+" : "-"}${fmt(t.amount)}  [${t.category}]`);
      console.log(`Balance: ${fmt(budget.balance())}`);
    } catch(e) { console.error(e.message); }
    break;
  }
  case "summary": {
    const { income, expense, balance } = budget.summary();
    console.log(`
Income:   ${fmt(income)}`);
    console.log(`Expenses: ${fmt(expense)}`);
    console.log(`Balance:  ${fmt(balance)}`);
    console.log("
By category:");
    Object.entries(budget.byCategory())
      .sort(([,a],[,b]) => b - a)
      .forEach(([key, val]) => console.log(`  ${key.padEnd(25)} ${fmt(val)}`));
    break;
  }
  case "list":
    if (!budget.all().length) { console.log("No transactions."); break; }
    budget.all().slice(-20).forEach(t => {
      const sign = t.type === "income" ? "+" : "-";
      console.log(`  ${t.date}  ${sign}${fmt(t.amount).padStart(14)}  [${t.category}]  ${t.note}`);
    });
    break;
  default:
    console.log("Commands: income <amount> [category] [note] | expense <amount> [category] [note] | list | summary");
}

4. Test & Prove

import { BudgetTracker } from "./budget.mjs";
let p=0,f=0;
function test(d,fn){try{fn();console.log("  ✓",d);p++;}catch(e){console.log("  ✗",d,"—",e.message);f++;}}
const b = new BudgetTracker();
b.add({ type:"income",  amount:5000, category:"Salary"  });
b.add({ type:"expense", amount:1200, category:"Rent"    });
b.add({ type:"expense", amount:300,  category:"Food"    });
test("balance is correct",   () => { if(b.balance()!==3500) throw new Error(b.balance()); });
test("summary totals",       () => { const s=b.summary(); if(s.income!==5000||s.expense!==1500) throw new Error(JSON.stringify(s)); });
test("byCategory keys",      () => { const c=b.byCategory(); if(!("income:Salary" in c)) throw new Error(); });
test("income:Salary = 5000", () => { if(b.byCategory()["income:Salary"]!==5000) throw new Error(); });
test("negative amount throws",() => { try{b.add({type:"expense",amount:-100,category:"x"});throw new Error("no");}catch(e){if(e.message==="no")throw e;} });
test("bad type throws",       () => { try{b.add({type:"gift",amount:100,category:"x"});throw new Error("no");}catch(e){if(e.message==="no")throw e;} });
console.log(`
${p} passed, ${f} failed`);

5. The Interface

node budget.mjs income  50000 Salary
node budget.mjs expense  8000 Rent "Monthly rent"
node budget.mjs expense  2500 Food "Groceries"
node budget.mjs summary

Income:   ₹50,000.00
Expenses: ₹10,500.00
Balance:  ₹39,500.00

6. Run It

node budget.mjs income 50000 Salary
node budget.mjs expense 8000 Rent
node budget.mjs summary
node budget.test.mjs

🎯 Try this next

  1. Add monthly filtering: node budget.mjs summary --month 2026-06
  2. Add budget limits per category — warn when spending exceeds 80% of limit
  3. Export to CSV: node budget.mjs export > june.csv
  4. Add a chart command: ASCII bar chart of spending by category

What you practised

Reduce for financial aggregation, groupBy accumulator pattern, Intl.NumberFormat for currency, ISO date string slicing for month grouping, and private class fields to protect transaction data.

Reference: Arrays · Date & Intl · OOP & Classes

Try It Yourself