Expense Tracker logs expenses with a description, amount, and category, then lets you view, edit, delete, search, filter, sort, and export them, with a live dashboard summarizing spend as it changes. It runs on a single Express server with no database by design — all data lives in memory and resets on restart — a deliberate choice to master Node/Express fundamentals before introducing persistence.
What makes it work
-
A derivation pipeline that can't drift.
currentExpensesis the only source of truth.computeVisibleExpenses()always derives a fresh filtered/searched/sorted array rather than mutating or caching one, and the dashboard stats are always computed from the full set — never the filtered view — so the numbers can't disagree with what's on screen. -
One row, three states, no double-edits.
Each expense row is view, editing, or confirming delete, tracked via CSS classes and
datasetattributes, with only one row ever allowed in a non-view state at a time.
Design at a glance
- bg · #f5f5f7
- surface · #ffffff
- primary · #0071e3
- danger · #d70015
- text · #1d1d1f
System font stack throughout, CSS custom properties driving the whole design system.
- Expense list — pending capture
Built to last
- CSV export always exports the full dataset regardless of active search/filter, with correct comma/quote escaping
- The server never trusts the client — identical validation rules enforced on both
POSTandPUT - 12 milestones, each shipped, manually tested, and approved before the next began
Want a tool built with this much discipline around state management?
Start a projectFor developers — the full technical breakdown
01. In-memory storage, deliberately
- Problem
- Introducing a database from milestone one would shift focus away from learning core Express/Node fundamentals, the actual goal of the project.
- Choice
- A single in-memory array on the server, with the explicit, documented tradeoff that data resets whenever the server restarts.
- Result
- A real, working REST API to build the frontend against, with persistence deliberately deferred as a named next step rather than skipped silently.
02. Non-mutating derivation instead of cached filtered state
- Problem
- Caching a "currently filtered" list alongside the full list risks the two silently drifting out of sync as data changes.
- Choice
computeVisibleExpenses()always derives a fresh array fromcurrentExpenseson every render; nothing is ever stored pre-filtered.- Result
- The dashboard stats and the visible list can never disagree with each other, regardless of what's currently searched, filtered, or sorted.
Accessibility & performance
- Server-side validation on
description/amount/categoryenforced identically on create and update, never trusting client input alone - Friendly empty, loading, and error states throughout, not silent failures
- Inline delete confirmation prevents accidental data loss
- Responsive layout, usable on both desktop and mobile
What I’d do differently
In-memory storage was the right call for a fundamentals-focused learning project, but it's the most obvious next step for a v2 — real persistence would also unlock the authentication and multi-user scoping that's currently out of scope.
Stack
- Node.js
- Express 5
- Vanilla JavaScript (ES2017+)
- HTML5 / CSS3