A full-stack personal expense tracker. Record, view, filter, and summarize expenses with exact decimal precision and resilience to network failures.
# Backend
cd backend && npm install
# Frontend
cd frontend && npm installcd backend
npm run devThe API server starts on http://localhost:3000.
cd frontend
npm run devThe dev server starts on http://localhost:5173 (or the next available port).
# Backend tests
cd backend
npm test
# Frontend tests
cd frontend
npm testMonetary amounts are stored as integer cents rather than floating-point decimals. For example, ₹12.50 is stored as 1250. This sidesteps IEEE 754 precision errors — integer arithmetic in floating point is exact, so 1250 + 750 === 2000 is always true, whereas 12.50 + 7.50 can drift. Conversion between decimal strings and integer cents happens at the API boundary: the client sends "12.50", the server multiplies by 100 and rounds, stores 1250, and the client divides by 100 for display.
The client generates a UUID when the expense form mounts and sends it as an Idempotency-Key request header on every POST. The server stores each processed key alongside its response in the idempotency_keys table. If the same key arrives again (retry, double-click, page reload mid-submit), the server returns the cached response immediately without creating a duplicate record. Keys expire after 24 hours. The insert uses INSERT OR IGNORE so concurrent duplicate requests are handled atomically.
SQLite was chosen for simplicity and durability. A single file (backend/expenses.db) holds all data and survives server restarts with no external process required. The better-sqlite3 driver exposes a synchronous API, which keeps the route handlers straightforward — no async/await chains around database calls. WAL (Write-Ahead Logging) mode is enabled so reads do not block writes.
The frontend is plain React with Vite. No state management library is needed given the small scope — component-local useState and a single top-level fetch on mount are sufficient. Vite provides fast HMR during development and a small production bundle.
Category filtering is implemented at the SQL level using LOWER(category) = LOWER(?). This avoids pulling all rows into JavaScript and filtering in memory, and it handles mixed-case input (e.g., food, Food, FOOD all match the same expenses).
The default sort is ORDER BY date DESC, created_at DESC. The secondary created_at key ensures deterministic ordering when two expenses share the same date — the more recently created record appears first. This is applied server-side so the client always receives a stable, predictable list.
| Decision | What was chosen | What was not chosen | Rationale |
|---|---|---|---|
| Database | SQLite | PostgreSQL, MySQL | Sufficient for a single-user personal tool. No separate process, no connection pooling needed. Would need PostgreSQL for multi-user or high-concurrency scenarios. |
| Idempotency store | Same SQLite DB | Redis, Memcached | Keeps the stack simple — one dependency instead of two. The downside is that idempotency keys are lost if the database file is deleted. A Redis store would be more robust in production. |
| Decimal precision | Integer cents | DECIMAL SQL type, decimal.js library |
Integer arithmetic is exact and requires no extra library. The conversion is a single Math.round(parseFloat(value) * 100) at the boundary. |
| Frontend state | Component-local state | Redux, Zustand, React Query | The app has one list and one form. A global store would add complexity with no benefit at this scale. |
- User authentication / authorization — out of scope for this exercise. All expenses are visible to anyone with access to the running server.
- Pagination — the list endpoint returns all expenses. Acceptable for personal use; would need cursor- or offset-based pagination at scale.
- Expense editing or deletion — expenses are append-only. Editing and deletion were not part of the requirements.
- Currency selection — the currency symbol is hardcoded to ₹. Supporting multiple currencies would require storing a currency code per expense and handling conversion rates.