Skip to content

Clean Code Principles in Unwrap ​

Clean code principles applied in this codebase, with links to where each one appears. Organised against the SE_08 module syllabus.

1. Good identifier names ​

Names that explain intent without comments.

2. Good functions: size, arguments, return values ​

Small functions, few arguments, one well-defined return.

3. Appropriate use of data types ​

Types narrow the set of valid states so invalid ones cannot be represented.

  • TimeRange union in src/types/api.ts. A finite set of valid time ranges ('7d' | '30d' | 'all'), used in services, query keys, and UI selectors.
  • DashboardCategory union. Every valid category is enumerated, so CATEGORY_LABELS: Record<DashboardCategory, string> in src/stores/dashboardStore.ts is compiler-enforced to be exhaustive.
  • ApiError extends Error in src/services/api.ts. A typed class with status: number so callers can branch on HTTP status without parsing strings.

4. Error handling and exceptions ​

Errors are typed, thrown at the right boundary, surfaced at the right layer.

  • ApiError and apiFetch in src/services/api.ts. Non-2xx responses throw a typed error with the HTTP status. Service callers never inspect raw Response objects.
  • useInitAuth in src/hooks/useInitAuth.ts. The .catch(() => setUser(null)) treats a 401 as "logged out" rather than crashing the app.
  • handleSave and handleDeleteAccount in src/pages/Settings.tsx. try/catch where the user sees the failure via setErrorMessage, with instanceof Error narrowing instead of as any.

5. Single Responsibility Principle (SRP) ​

Every module has one reason to change.

6. Principle of Least Astonishment ​

Code behaves the way the name and signature suggest.

  • fetchUser() in src/services/api.ts. Fetches the user. No hidden logout, no clearing of state. A 401 surfaces as an ApiError and the caller decides what it means.
  • toggleCategory in src/stores/dashboardStore.ts. Toggles a category. The one non-obvious rule (refusing to remove the last one) is documented inline.
  • setUser in src/stores/authStore.ts. Sets the user and derives isAuthenticated from it. The two cannot drift out of sync.

7. Modularity, cohesion, coupling ​

Modules organised so changes stay localised.

  • Folder-per-component layout. src/components/RankedList/ holds the .tsx and the scoped .module.css together. No global CSS coupling.
  • src/config/env.ts. All env-var reading goes through one file. The rest of the codebase imports API_BASE_URL and USE_MOCKS and never reaches into import.meta.env.
  • The dependency arrow points one way: components depend on hooks, hooks on services, services on apiFetch. A component change cannot break the service layer.

8. Encapsulation and information hiding ​

Modules expose only what callers need.

  • apiFetch, buildParams, and ApiFetchOptions in src/services/api.ts are not exported. Callers get the typed fetchTopTracks, fetchUser, etc., plus ApiError. Swapping fetch for axios would touch only this file.
  • partialize: (state) => ({ user: state.user }) in src/stores/authStore.ts. The persistence layer only writes user to localStorage. isLoading and isAuthenticated stay internal.
  • DEFAULT_CATEGORIES in src/stores/dashboardStore.ts is not exported. The only way back to defaults is via resetToDefaults(). The behaviour is the public surface, not the array.

9. Interfaces and contracts ​

Types describe the contract a caller can rely on, independent of implementation.

  • RankedItem interface in src/components/RankedList/RankedList.tsx. Defines what every list item must look like. TrackList, ArtistList, GenreList, SkipList, and ReplayList map their domain data to this shape.
  • AuthState and DashboardState in src/stores/. Every store declares an explicit interface up front, so the public surface is one block of types.
  • src/types/api.ts. One file holds the contract between frontend and backend. Every service return type, hook generic, and component prop pulls from here.

10. DRY (Don't Repeat Yourself) ​

Shared abstractions instead of copy-pasted variants.

  • RankedList in src/components/RankedList/RankedList.tsx. One component renders top tracks, artists, genres, skips, and replays. Five thin adapters (e.g. TrackList.tsx, SkipList.tsx) feed it domain data.
  • apiFetch in src/services/api.ts. One wrapper handles URL building, content-type, error wrapping, and JSON parsing for every service function.
  • Skeleton at src/components/Skeleton/Skeleton.tsx. One shimmer primitive used by the dashboard, lists, history, and Wrapped. The shimmer animation lives in CSS once.

11. Pure functions and referential transparency ​

Functions with no side effects, easy to test.

12. Single source of truth ​

For any fact, exactly one place defines it.

  • CATEGORY_LABELS in src/stores/dashboardStore.ts. Defines every valid dashboard category and its display label. ALL_CATEGORIES is derived from its keys, and the CategoryPicker UI (src/components/CategoryPicker/CategoryPicker.tsx) reads from it. Add a category in one place; picker, persistence, and dashboard pick it up.
  • src/config/env.ts. Every build-time flag (USE_MOCKS, IS_PLAYWRIGHT, API_BASE_URL) is read here once.
  • src/types/api.ts. The response shape for every endpoint is defined once. Service, MSW handler, hook, and component are all type-checked against it.

13. YAGNI (You Aren't Gonna Need It) ​

Deliberate omissions in favour of the simplest thing that works.

  • No Redux. Zustand was chosen because client state is small. See src/stores/. Rationale in Architecture: trade-offs.
  • MSW instead of a parallel mock backend. Handlers in src/mocks/ cover every endpoint, so the frontend can be developed without waiting on the backend repo.
  • No SSR, no GraphQL, no global event bus. Each rejected with reasons in Architecture: trade-offs.

Built for SE_07 — Technical Documentation