Trackrr
Personal expense tracker with monthly budget per category — REST API + Next.js front.
Back-end (API)
- Express 5
- TypeScript
- Prisma 7
- PostgreSQL
- Zod
- JWT
- bcryptjs
- Pino
- Vitest
- Supertest
Front-end
- Next.js 16
- React
- TypeScript
- TanStack Query
- Zustand
- Axios
- Zod
Full-screen for best quality.
Context
Trackrr is a personal expense tracker with monthly budget control per category. Users register categories, log expenses and set a monthly cap per category. The dashboard shows spent vs. budget in real time, always derived from expenses — never stored. Export to PDF (via @react-pdf/renderer on the client) and CSV (no lib, generated in plain code).
The back-end follows a strict layered architecture with Express 5, Prisma 7 and PostgreSQL. Monetary values use Decimal(12,2) serialized as string in JSON. The test suite runs 79 tests in ~1 second with Prisma mocked, no database dependency.
The front-end uses Next.js 16 with TanStack Query for server state and Zustand exclusively for auth. Dual authentication: JWT in the header via Axios interceptor + mirror cookie for the edge middleware. Feature-based folder structure, not type-based.
Technical decisions
API · Strict layered architecture
The app follows Routes → Controllers → Services → Repositories, each layer with a single responsibility. Controllers never touch Prisma, services never manipulate req/res, and repositories hold no business rules. Dependencies flow in one direction.
Repositories can be mocked to test services in isolation; the Prisma client can be mocked to test routes end-to-end. Business rules stay in services, validations in schemas, and persistence changes never leak out.
API · Decimal for monetary values
All monetary fields use Decimal(12,2) in Prisma and are serialized as strings in JSON (“12.50” instead of 12.5). This avoids IEEE 754 precision issues — 0.1 + 0.2 === 0.30000000000000004 in JS — which accumulate in budget operations.
Serializing as string preserves precision until the consumer, which can choose the right representation for its context (libs like decimal.json the front, BigInt for exact math, or float conversion when precision isn’t critical).
API · Testing strategy with Prisma mocked
The suite (Vitest + Supertest) runs 79 tests in ~1 second with no live database. The Prisma client is mocked via vi.mock, and each test explicitly configures the behavior of the consumed methods. Coverage includes routes, middlewares, services, mappers — the whole stack above persistence.
Trade-off: SQL bugs or broken migrations aren’t caught here. The complementary layer would be repository tests against real Postgres (testcontainers) in the CI pipeline.
Front · Zustand for auth, TanStack Query for everything else
UI state and server state are handled by different tools. Server state uses TanStack Query — cache, invalidation, refetch and in-flight dedup.
Zustand is used only where Query doesn’t fit: the { token, user } auth pair, which must be synchronous, persisted in localStorage and readable from outside-component code (Axios request interceptor). Server state and client state stay in distinct layers.
Front · Feature-based architecture, not type-based
features/expenses/ bundles everything for the feature: api.ts, hooks.ts, schemas.ts, types.ts, components/. The app/ folder stays thin — each page is composition of these hooks and components.
Rule: features don’t import from one another. Anything cross-cutting goes to shared/. The blast radius of any change stays contained within the feature folder.
Front · Client-side PDF generation with lazy import
The easier path would be adding Puppeteer/Playwright to the Express API and rendering PDFs on the back-end — pixel-perfect, any CSS works, but the cost would be too high.
@react-pdf/renderer on the client: JSX components become PDF binary directly in the browser, without rasterizing the DOM (which is what html2canvasdoes, and it comes out pixelated). The API stays decoupled from the format — if tomorrow export to Excel or Markdown is needed, it’s just another module in features/export/, without touching the back-end.
Run locally
Requires: Docker and Git installed.
mkdir trackrr-local && cd trackrr-local
git clone https://github.com/odgiedev/trackrr
git clone https://github.com/odgiedev/trackrr-api
cp trackrr/.env.example trackrr/.env
cp trackrr-api/.env.example trackrr-api/.env
# Terminal 1 — API
cd trackrr-api && docker compose up
# Terminal 2 — Front
cd trackrr && docker compose up