Expense Tracker
A full-stack personal finance app for tracking income, expenses, and savings with your own categories, monthly reports, and interactive charts.
React · TypeScript · Vite · Tailwind CSS · Radix UI · Recharts · React Hook Form · Zod · Express · Prisma · PostgreSQL · JWT
The problem
Most budget apps lock you into fixed categories and only split money two ways — in or out. Savings get counted as spending, so the numbers lie. I needed a model where savings is a first-class transaction type, categories are fully user-owned, and every report recomputes instantly as data changes.
The approach
I built a React + TypeScript SPA on Vite with Tailwind and Radix UI primitives, backed by an Express + Prisma + PostgreSQL API. Auth is email/password with a JWT in an httpOnly cookie. Transactions carry a three-way type (income / expense / savings) mirrored from their category for fast queries, and all reporting is derived client-side from a single transactions fetch — no reporting endpoints, no stale aggregates.
Architecture
// Monorepo
├── /frontend # React + Vite + TypeScript SPA
│ └── /src
│ ├── /pages # Dashboard, Transactions, Categories, Reports, Settings, Login, Register
│ ├── /charts # Recharts: income vs expense, savings trend, category pie/bar
│ ├── /components # Reusable UI built on Radix primitives
│ ├── /services # Typed API client over /api
│ ├── /context # Auth + currency providers
│ ├── /hooks # Data fetching and derived-stat hooks
│ └── /lib # Types, formatters, transaction helpers
└── /backend # Express + Prisma REST API
├── /src
│ ├── /routes # auth, categories, transactions
│ ├── auth.ts # JWT issue/verify, httpOnly cookie
│ ├── defaults.ts # Starter categories seeded on register
│ └── prisma.ts # Prisma client singleton
└── /prisma # schema.prisma + migrations (User, Category, Transaction)
What it moved
- Three-way money model (income / expense / savings) makes savings rate an accurate, first-class metric instead of a mislabelled expense
- User-owned categories with typed colors — rename, recolor, or delete without touching the schema
- Zero reporting endpoints: dashboard, savings trend, and category breakdowns all derive from one cached fetch, so charts update the instant a transaction is added
- End-to-end API smoke test (npm run smoke) covers the full auth → category → transaction flow