Skip to content
Personal

Expense Tracker

A full-stack expense tracker built from a bare Express server up to a polished, portfolio-ready app — 12 milestones, each shipped and tested before the next began.

Role
Design & development, solo
Year
2026
Stack
Node.js, Express, vanilla JS
Status
Complete

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. currentExpenses is 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 dataset attributes, 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 POST and PUT
  • 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 project
For 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 from currentExpenses on 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/category enforced 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