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.
formatMinutes(ms: number): stringin src/utils/formatters.ts. Input unit, output unit, and behaviour are all readable from the signature.useInitAuthin src/hooks/useInitAuth.ts. Theuseprefix marks it as a React hook.InitplusAuthsays what it does and when it runs.clearPersistedStoresin src/test/clearPersistedStores.ts. Verb plus scope in three words.
2. Good functions: size, arguments, return values
Small functions, few arguments, one well-defined return.
formatHour(hour: number): stringin src/utils/formatters.ts. Five lines, one argument, four explicit branches.buildParamsin src/services/api.ts. Object in,URLSearchParamsout, undefined values filtered.useDemoLoginin src/hooks/useDemoLogin.ts. Returns one closure that sets the user and navigates to the dashboard.
3. Appropriate use of data types
Types narrow the set of valid states so invalid ones cannot be represented.
TimeRangeunion insrc/types/api.ts. A finite set of valid time ranges ('7d' | '30d' | 'all'), used in services, query keys, and UI selectors.DashboardCategoryunion. Every valid category is enumerated, soCATEGORY_LABELS: Record<DashboardCategory, string>in src/stores/dashboardStore.ts is compiler-enforced to be exhaustive.ApiError extends Errorin src/services/api.ts. A typed class withstatus: numberso 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.
ApiErrorandapiFetchin src/services/api.ts. Non-2xx responses throw a typed error with the HTTP status. Service callers never inspect rawResponseobjects.useInitAuthin src/hooks/useInitAuth.ts. The.catch(() => setUser(null))treats a 401 as "logged out" rather than crashing the app.handleSaveandhandleDeleteAccountin src/pages/Settings.tsx. try/catch where the user sees the failure viasetErrorMessage, withinstanceof Errornarrowing instead ofas any.
5. Single Responsibility Principle (SRP)
Every module has one reason to change.
- The three-layer pattern. Services do HTTP only (src/services/api.ts), hooks manage query lifecycle only (src/hooks/useConnectedServices.ts), components render only (src/components/PeakHourCard/PeakHourCard.tsx). API change touches services and types. Caching change touches hooks. Neither affects components.
- src/utils/formatters.ts. Every exported function does one transform. Adding a new format never modifies an existing function.
authStoreanddashboardStoreinsrc/stores/. Auth and UI prefs live in separate stores. Logging out cannot accidentally reset the dashboard layout.
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 anApiErrorand the caller decides what it means.toggleCategoryin src/stores/dashboardStore.ts. Toggles a category. The one non-obvious rule (refusing to remove the last one) is documented inline.setUserin src/stores/authStore.ts. Sets the user and derivesisAuthenticatedfrom 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
.tsxand the scoped.module.csstogether. No global CSS coupling. - src/config/env.ts. All env-var reading goes through one file. The rest of the codebase imports
API_BASE_URLandUSE_MOCKSand never reaches intoimport.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, andApiFetchOptionsin src/services/api.ts are not exported. Callers get the typedfetchTopTracks,fetchUser, etc., plusApiError. Swappingfetchforaxioswould touch only this file.partialize: (state) => ({ user: state.user })in src/stores/authStore.ts. The persistence layer only writesuserto localStorage.isLoadingandisAuthenticatedstay internal.DEFAULT_CATEGORIESin src/stores/dashboardStore.ts is not exported. The only way back to defaults is viaresetToDefaults(). 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.
RankedIteminterface in src/components/RankedList/RankedList.tsx. Defines what every list item must look like.TrackList,ArtistList,GenreList,SkipList, andReplayListmap their domain data to this shape.AuthStateandDashboardStateinsrc/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.
RankedListin 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.apiFetchin src/services/api.ts. One wrapper handles URL building, content-type, error wrapping, and JSON parsing for every service function.Skeletonatsrc/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.
- All four functions in src/utils/formatters.ts:
formatMinutes,formatDate,formatHour,formatDayMonth. Same input, same output. Tests in src/tests/utils/formatters.test.ts need zero setup. buildParamsin src/services/api.ts. Record in,URLSearchParamsout. No I/O, no mutation of the input.
12. Single source of truth
For any fact, exactly one place defines it.
CATEGORY_LABELSin src/stores/dashboardStore.ts. Defines every valid dashboard category and its display label.ALL_CATEGORIESis derived from its keys, and theCategoryPickerUI (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.