module cleanup
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user