Overview
This page is a 5-minute orientation to what Unwrap is, who it's for, and how the codebase is shaped at a high level. Once you've read it, you should know enough to decide where to dig in next.
NOTE
If you don't have Unwrap running yet, start with Get Started. If you want the deep mechanical detail, jump to Architecture.
What Unwrap is
Unwrap is a Spotify listening history analytics web app, a customisable alternative to Spotify Wrapped. Users connect their Spotify account, upload their lifetime listening export, and explore stats on a dashboard they shape themselves.
This repository is the frontend: a React single-page application. The backend lives in a separate repository, maintained by other team members. The two communicate over HTTP.
For the end-user experience, see the User Guide.
Three things to know before reading code
1. It's a pure SPA
Everything user-facing lives in the browser. There's no server-side rendering, no Next.js, no shared runtime with the backend. The frontend builds to static files and is deployed to Vercel. All dynamic behaviour comes from the backend API or from client-side state.
2. There's a strict three-layer pattern
Every feature in Unwrap is built the same way: a Component uses a Hook, which calls a Service.
If you're touching anything that involves data, expect to work across all three layers. The Architecture page explains why.
3. The backend is optional in development
Unwrap can run end-to-end with no backend at all. MSW intercepts every fetch call in dev mode and returns realistic mock data. This means:
- You can develop the frontend without waiting on backend work.
- The same mocks power the integration test suite.
- The deployed demo runs entirely in MSW mode; no real Spotify, no real backend.
The trade-off: anything that needs real data has to be tested against the live backend separately. See the dev vs production model.
How the codebase is shaped
src/
├── components/ # Reusable UI; one folder per component
├── pages/ # Top-level route components
├── hooks/ # TanStack Query data hooks
├── services/ # HTTP functions (one file per API domain)
├── stores/ # Zustand stores (auth + dashboard prefs)
├── mocks/ # MSW handlers + fixtures (dev + tests)
├── types/ # TypeScript interfaces for API shapes
├── utils/ # Pure helpers (formatters, etc.)
└── config/ # Env-var readingA quick mental model for what goes where:
| If you're working on… | You'll mostly be in… |
|---|---|
| A new visual feature | components/ and maybe pages/ |
| Wiring up new server data | services/, hooks/, and types/ |
| A new mock or fixture for testing | mocks/handlers.ts, mocks/fixtures.ts |
| Auth or UI preferences | stores/ |
| Anything that's a pure transform | utils/ |
How decisions were made
A few principles drove the architecture. Knowing them up front makes the rest of the codebase make sense:
- Single Responsibility everywhere. Each layer does one thing. Each store has one concern. Each service file covers one API domain.
- Server state and client state are different things. TanStack Query owns server state; Zustand owns client state. They never overlap.
- Test at the network boundary, not below it. MSW intercepts
fetchso tests exercise the real component, hook, and service code. Don't mock what you own. - Type everything that crosses a layer. Service return types, store interfaces, hook results.
anyis forbidden by ESLint. - No co-located tests. Tests mirror
src/undersrc/__tests__/. Source stays clean; finding a test is one folder away.