module cleanup

This commit is contained in:
2025-12-19 11:32:16 +01:00
parent d0fac9e6c1
commit c064b597cc
2 changed files with 209 additions and 342 deletions

View File

@@ -1,361 +1,207 @@
Frontend data shapes: View Models, Presenters, and API Client (Strict)
Frontend & Backend Output Shapes Clean Architecture (Strict, Final)
This document defines the exact placement and responsibilities for frontend data shapes.
It is designed to leave no room for interpretation.
This document defines the exact responsibilities, naming, and placement of all data shapes involved in delivering data from Core → API → Frontend UI.
It resolves all ambiguity around Presenters, View Models, DTOs, and Output Ports.
There is no overlap of terminology across layers.
1. Definitions
1. Core Layer (Application / Use Cases)
API DTOs
Core Output Ports (formerly “Presenters”)
Transport shapes owned by the API boundary (HTTP). They are not used directly by UI components.
In the Core, a Presenter is not a UI concept.
View Models
It is an Output Port that defines how a Use Case emits its result.
UI-owned shapes. They represent exactly what the UI needs and nothing else.
Rules
• Core Output Ports:
• define what data is emitted
• do not store state
• do not expose getters
• do not reference DTOs or View Models
• Core never pulls data back from an output port
• Core calls present() and stops
Website Presenters
Naming
• *OutputPort
• *Result (pure application result)
Pure mappers that convert API DTOs (or core use-case outputs) into View Models.
Example
API Client
export interface CompleteDriverOnboardingResult {
readonly success: boolean;
readonly driverId?: string;
readonly error?: string;
}
A thin HTTP wrapper that returns API DTOs only and performs no business logic.
export interface CompleteDriverOnboardingOutputPort {
present(result: CompleteDriverOnboardingResult): void;
}
The Core does not know or care what happens after present() is called.
2. Directory layout (exact)
2. API Layer (Delivery / Adapter)
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
API Presenters (Response Mappers)
No additional folders for these concerns are allowed.
API Presenters are Adapters.
They:
• implement Core Output Ports
• translate Core Results into API Response DTOs
• store response state temporarily for the controller
They are not View Models.
Rules
• API Presenters:
• implement a Core Output Port
• map Core Results → API Responses
• may store state internally
• API Presenters must not:
• contain business logic
• reference frontend View Models
Naming
• *Presenter or *ResponseMapper
• Output types end with Response or ApiResponse
3. View Models (placement and rules)
3. Frontend Layer (apps/website)
Where they live
View Models (UI-Owned, Final Form)
View Models MUST live in:
A View Model represents fully prepared UI state.
apps/website/lib/view-models
Only the frontend has Views — therefore only the frontend has 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)
Rules
View Models:
live only in apps/website
accept API Response DTOs as input
• expose UI-ready data and helpers
• View Models must not:
• contain domain logic
• validate business rules
• perform side effects
• be sent back to the server
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.
Naming
*ViewModel
4. API DTOs in the website (placement and rules)
4. Website Presenters (DTO → ViewModel)
Clarification
Website Presenters are pure mappers.
The website does have DTOs, but only API DTOs.
They:
• convert API Response DTOs into View Models
• perform formatting and reshaping
• are deterministic and side-effect free
These DTOs exist exclusively to type HTTP communication with the backend API.
They are not UI models.
They are not Core Presenters.
Rules
• Input: API DTOs
• Output: View Models
• Must not:
• call APIs
• read storage
• perform decisions
Where they live
5. API Client (Frontend)
Website-side API DTO types MUST live in:
The API Client is a thin HTTP layer.
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
Rules
• 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.
• Returns API DTOs only
Must not:
return View Models
• contain business logic
• format data for UI
8. Website service layer (strict orchestration)
6. Website Services (Orchestration)
Where it lives
Website Services orchestrate:
• API Client calls
• Website Presenter mappings
Website orchestration MUST live in:
They are the only layer allowed to touch both.
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.
Rules
• Services:
• call API Client
call Website Presenters
return View Models only
Components never touch API Client or DTOs
9. Allowed dependency directions (frontend)
7. Final Data Flow (Unambiguous)
Within apps/website:
Core Use Case
→ OutputPort.present(Result)
components -> services -> (api + presenters) -> (dtos + view-models)
API Presenter (Adapter)
→ maps Result → ApiResponse
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
API Controller
→ returns ApiResponse (JSON)
Frontend API Client
→ returns ApiResponse DTO
Website Presenter
→ maps DTO → ViewModel
UI Component
→ consumes ViewModel
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)
8. Terminology Rules (Strict)
Term Layer Meaning
OutputPort Core Use case output contract
Result Core Pure application result
Presenter (API) apps/api Maps Result → API Response
Response / ApiResponse apps/api HTTP transport shape
Presenter (Website) apps/website Maps DTO → ViewModel
ViewModel apps/website UI-ready state
No term is reused with a different meaning.
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
9. Non-Negotiable Rules
Core has no DTOs
Core has no View Models
API has no View Models
Frontend has no Core Results
View Models exist only in the frontend
Presenters mean different things per layer, but:
• Core = Output Port
• API = Adapter
• Website = Mapper
12. Clean Architecture Flow Diagram
10. Final Merksatz
```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
The Core emits results.
The API transports them.
The Frontend interprets them.
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
}));
}
}
```
If a type tries to do more than one of these — it is incorrectly placed.