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