Frontend data shapes: View Models, Presenters, and API Client (Strict) This document defines the exact placement and responsibilities for frontend data shapes. It is designed to leave no room for interpretation. ⸻ 1. Definitions API DTOs Transport shapes owned by the API boundary (HTTP). They are not used directly by UI components. View Models UI-owned shapes. They represent exactly what the UI needs and nothing else. Website Presenters Pure mappers that convert API DTOs (or core use-case outputs) into View Models. API Client A thin HTTP wrapper that returns API DTOs only and performs no business logic. ⸻ 2. Directory layout (exact) apps/website ├── app/ # Next.js routes/pages ├── components/ # React components (UI only) ├── lib/ │ ├── api/ # API client (HTTP only) │ ├── dtos/ # API DTO types (transport shapes) │ ├── view-models/ # View Models (UI-owned shapes) │ ├── presenters/ # Presenters: DTO -> ViewModel mapping │ ├── services/ # UI orchestration (calls api + presenters) │ └── index.ts No additional folders for these concerns are allowed. ⸻ 3. View Models (placement and rules) Where they live View Models MUST live in: apps/website/lib/view-models What they may contain • UI-ready primitives (strings, numbers, booleans) • UI-specific derived fields (e.g., isOwner, badgeLabel, formattedDate) • UI-specific structures (e.g., grouped arrays, flattened objects) What they must NOT contain • Domain entities or value objects • API transport metadata • Validation logic • Network or persistence concerns Rule Components consume only View Models. ⸻ 4. API DTOs in the website (placement and rules) Clarification The website does have DTOs, but only API DTOs. These DTOs exist exclusively to type HTTP communication with the backend API. They are not UI models. ⸻ Where they live Website-side API DTO types MUST live in: apps/website/lib/dtos What they represent • Exact transport shapes sent/received via HTTP • Backend API contracts • No UI assumptions Who may use them • API client • Website presenters Who must NOT use them • React components • Pages • UI logic Rule API DTOs stop at the presenter boundary. Components must never consume API DTOs directly. ⸻ 5. Presenters (website) (placement and rules) Where they live Website presenters MUST live in: apps/website/lib/presenters What they do • Convert API DTOs into View Models • Perform UI-friendly formatting and structuring • Are pure and deterministic What they must NOT do • Make API calls • Read from localStorage/cookies directly • Contain business rules or decisions • Perform side effects Rule Presenters output View Models. Presenters never output API DTOs. ⸻ 6. Do website presenters use View Models? Yes. Strictly: Website presenters MUST output View Models and MUST NOT output API DTOs. Flow is always: API DTO -> Presenter -> View Model -> Component ⸻ 7. API client (website) (placement and rules) Where it lives The API client MUST live in: apps/website/lib/api What it does • Sends HTTP requests • Returns API DTOs • Performs authentication header/cookie handling only at transport level • Does not map to View Models What it must NOT do • Format or reshape responses for UI • Contain business rules • Contain decision logic Rule The API client has no knowledge of View Models. ⸻ 8. Website service layer (strict orchestration) Where it lives Website orchestration MUST live in: apps/website/lib/services What it does • Calls the API client • Calls presenters to map DTO -> View Model • Returns View Models to pages/components What it must NOT do • Contain domain logic • Modify core invariants • Return API DTOs Rule Services are the only layer allowed to call both api/ and presenters/. Components must not call the API client directly. ⸻ 9. Allowed dependency directions (frontend) Within apps/website: components -> services -> (api + presenters) -> (dtos + view-models) Strict rules: • components may import only view-models and services • presenters may import dtos and view-models only • api may import dtos only • services may import api, presenters, view-models Forbidden: • components importing api • components importing dtos • presenters importing api • api importing view-models • any website code importing core domain entities ⸻ 10. Naming rules (strict) • View Models end with ViewModel • API DTOs end with Dto • Presenters end with Presenter • Services end with Service • One export per file • File name equals exported symbol (PascalCase) ⸻ 11. Final "no ambiguity" summary • View Models live in apps/website/lib/view-models • API DTOs live in apps/website/lib/dtos • Presenters live in apps/website/lib/presenters and map DTO -> ViewModel • API client lives in apps/website/lib/api and returns DTOs only • Services live in apps/website/lib/services and return View Models only • Components consume View Models only and never touch API DTOs or API clients ⸻ 12. Clean Architecture Flow Diagram ```mermaid graph TD A[UI Components] --> B[Services] B --> C[API Client] B --> D[Presenters] C --> E[API DTOs] D --> E D --> F[View Models] A --> F style A fill:#e1f5fe style B fill:#f3e5f5 style C fill:#fff3e0 style D fill:#e8f5e8 style E fill:#ffebee style F fill:#e3f2fd ``` **Flow Explanation:** - UI Components consume only View Models - Services orchestrate API calls and presenter mappings - API Client returns raw API DTOs - Presenters transform API DTOs into UI-ready View Models - Strict dependency direction: UI → Services → (API + Presenters) → (DTOs + ViewModels) ⸻ 13. Enforcement Guidelines **ESLint Rules:** - Direct imports from `apiClient` are forbidden - use services instead - Direct imports from `dtos` in UI components are forbidden - use ViewModels instead - Direct imports from `api/*` in UI components are forbidden - use services instead **TypeScript Path Mappings:** - Use `@/lib/dtos` for API DTO imports - Use `@/lib/view-models` for View Model imports - Use `@/lib/presenters` for Presenter imports - Use `@/lib/services` for Service imports - Use `@/lib/api` for API client imports **Import Restrictions:** - Components may import only view-models and services - Presenters may import dtos and view-models only - API may import dtos only - Services may import api, presenters, view-models - Forbidden: components importing api, components importing dtos, presenters importing api, api importing view-models **Verification Commands:** ```bash npm run build # Ensure TypeScript compiles npm run lint # Ensure ESLint rules pass npm run test # Ensure all tests pass ``` ⸻ 14. Architecture Examples **Before (Violates Rules):** ```typescript // In a page component - BAD import { apiClient } from '@/lib/apiClient'; import type { RaceResultDto } from '@/lib/dtos/RaceResultDto'; const RacePage = () => { const [data, setData] = useState(); // Direct API call and DTO usage in UI useEffect(() => { apiClient.getRaceResults().then(setData); }, []); return
{data?.map(d => d.position)}
; }; ``` **After (Clean Architecture):** ```typescript // In a page component - GOOD import { RaceResultsService } from '@/lib/services/RaceResultsService'; import type { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel'; const RacePage = () => { const [data, setData] = useState(); useEffect(() => { RaceResultsService.getResults().then(setData); }, []); return
{data?.map(d => d.formattedPosition)}
; }; ``` **Service Implementation:** ```typescript // apps/website/lib/services/RaceResultsService.ts import { apiClient } from '@/lib/api'; import { RaceResultsPresenter } from '@/lib/presenters/RaceResultsPresenter'; export class RaceResultsService { static async getResults(): Promise { const dtos = await apiClient.getRaceResults(); return RaceResultsPresenter.present(dtos); } } ``` **Presenter Implementation:** ```typescript // apps/website/lib/presenters/RaceResultsPresenter.ts import type { RaceResultDto } from '@/lib/dtos/RaceResultDto'; import type { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel'; export class RaceResultsPresenter { static present(dtos: RaceResultDto[]): RaceResultViewModel[] { return dtos.map(dto => ({ id: dto.id, formattedPosition: `${dto.position}${dto.position === 1 ? 'st' : dto.position === 2 ? 'nd' : dto.position === 3 ? 'rd' : 'th'}`, driverName: dto.driverName, // ... other UI-specific formatting })); } } ```