361 lines
8.8 KiB
Markdown
361 lines
8.8 KiB
Markdown
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
|
|
}));
|
|
}
|
|
}
|
|
``` |