This commit is contained in:
2025-12-17 23:14:07 +01:00
parent 6123264353
commit 236655ba9e
4 changed files with 187 additions and 1 deletions

View File

@@ -0,0 +1,361 @@
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<RaceResultDto[]>();
// Direct API call and DTO usage in UI
useEffect(() => {
apiClient.getRaceResults().then(setData);
}, []);
return <div>{data?.map(d => d.position)}</div>;
};
```
**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<RaceResultViewModel[]>();
useEffect(() => {
RaceResultsService.getResults().then(setData);
}, []);
return <div>{data?.map(d => d.formattedPosition)}</div>;
};
```
**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<RaceResultViewModel[]> {
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
}));
}
}
```

View File

@@ -0,0 +1,82 @@
# Display Objects
## Definition
A **Display Object** encapsulates **reusable, UI-only display logic**.
It answers the question:
> “How should this specific piece of information be shown?”
Display Objects are **not screen-specific**.
They exist to avoid duplicating presentation logic across View Models.
---
## Responsibilities
A Display Object MAY:
- format values (money, dates, durations)
- handle localization and language-specific rules
- map codes to labels
- encapsulate UI display conventions
- be reused across multiple View Models
A Display Object MUST:
- be deterministic
- be side-effect free
- operate only on presentation data
---
## Restrictions
A Display Object MUST NOT:
- contain business logic
- enforce domain invariants
- perform validation
- influence system behavior
- be sent back to the server
- depend on backend or infrastructure concerns
If a rule affects system correctness or persistence,
it does not belong in a Display Object.
---
## Ownership & Placement
- Display Objects belong to the **presentation layer**
- They are frontend-only
- They are not shared with the backend or core
---
## Relationship to View Models
- View Models MAY use Display Objects
- Display Objects MUST NOT depend on View Models
- Display Objects represent **parts**
- View Models represent **screens**
---
## Testing
Display Objects SHOULD be tested because they often contain:
- locale-specific behavior
- formatting rules
- edge cases visible to users
---
## Summary
- Display Objects encapsulate **how something looks**
- View Models encapsulate **what a screen needs**
- Both are presentation concerns
- Neither contains business truth

View File

@@ -0,0 +1,85 @@
+# View Models
## Definition
A **View Model** represents a **fully prepared UI state**.
It answers the question:
> “What does the UI need in order to render this screen without thinking?”
View Models are **UI-owned** classes.
They do not represent business truth and do not enforce domain rules.
---
## Responsibilities
A View Model MAY:
- accept an API DTO as input
- derive UI-specific fields
- combine or reshape data for rendering
- perform formatting (dates, numbers, labels)
- handle localization and presentation logic
- use Display Objects for reusable UI concerns
A View Model MUST:
- be fully usable by the UI without further computation
- expose only data and UI-oriented helpers
- be created in a consistent, explicit way
---
## Restrictions
A View Model MUST NOT:
- contain business logic
- validate domain rules
- enforce permissions or authorization
- contain domain entities or value objects
- perform side effects
- be sent back to the server
If a View Model decides whether something is *allowed* or *correct*,
that logic belongs in the Core, not here.
---
## Ownership & Placement
- View Models belong to the **frontend**
- They live close to the UI, not in shared or core layers
- They are not shared with the backend
---
## Creation Rules
- View Models are created from API DTOs
- UI components must never construct View Models themselves
- Construction happens in services or presentation layers
- The UI only consumes View Models, never DTOs
---
## Testing
View Models SHOULD be tested when they contain:
- formatting logic
- localization behavior
- non-trivial derived fields
View Models do NOT need tests if they only expose data without logic.
---
## Summary
- View Models describe **UI state**
- They are **presentation-focused**, not business-focused
- They reduce complexity in components
- They form a stable contract for the UI