This commit is contained in:
2025-12-19 11:54:59 +01:00
parent c064b597cc
commit 08ec2af5bf
3 changed files with 490 additions and 136 deletions
+4
View File
@@ -91,6 +91,10 @@
{
"selector": "ExportDefaultDeclaration",
"message": "Default exports are forbidden. Use named exports instead."
},
{
"selector": "TSInterfaceDeclaration[id.name=/^I[A-Z]/]",
"message": "Interface names should not start with 'I'. Use descriptive names without the 'I' prefix (e.g., 'LiverCompositor' instead of 'ILiveryCompositor')."
}
]
}
+371 -136
View File
@@ -1,207 +1,442 @@
Frontend & Backend Output Shapes – Clean Architecture (Strict, Final)
Clean Architecture – Application Services, Use Cases, Ports, and Data Flow (Strict, Final)
This document defines the exact responsibilities, naming, and placement of all data shapes involved in delivering data from Core → API → Frontend UI.
This document defines the final, non-ambiguous Clean Architecture setup for the project.
It resolves all ambiguity around Presenters, View Models, DTOs, and Output Ports.
There is no overlap of terminology across layers.
It explicitly covers:
• Use Cases vs Application Services
• Input & Output Ports (and what does not exist)
• API responsibilities
• Frontend responsibilities
• Naming, placement, and dependency rules
• End-to-end flow with concrete paths and code examples
There are no hybrid concepts, no overloaded terms, and no optional interpretations.
⸻
1. Core Layer (Application / Use Cases)
1. Architectural Layers (Final)
Core Output Ports (formerly “Presenters”)
Domain → Business truth
Application → Use Cases + Application Services
Adapters → API, Persistence, External Systems
Frontend → UI, View Models, UX logic
In the Core, a Presenter is not a UI concept.
Only dependency-inward is allowed.
It is an Output Port that defines how a Use Case emits its result.
⸻
2. Domain Layer (Core / Domain)
What lives here
• Entities (classes)
• Value Objects (classes)
• Domain Services (stateless business logic)
• Domain Events
• Domain Errors / Invariants
What NEVER lives here
• DTOs
• Models
• Ports
• Use Cases
• Application Services
• Framework code
⸻
3. Application Layer (Core / Application)
The Application Layer has two distinct responsibilities:
1. Use Cases – business decisions
2. Application Services – orchestration of multiple use cases
⸻
4. Use Cases (Application / Use Cases)
Definition
A Use Case represents one business intent.
Examples:
• CreateLeague
• ApproveSponsorship
• CompleteDriverOnboarding
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
• A Use Case:
• contains business logic
• enforces invariants
• operates on domain entities
• communicates ONLY via ports
• A Use Case:
• does NOT orchestrate multiple workflows
• does NOT know HTTP, UI, DB, queues
Naming
• *OutputPort
• *Result (pure application result)
Structure
core/racing/application/use-cases/
└─ CreateLeagueUseCase.ts
Example
export interface CompleteDriverOnboardingResult {
readonly success: boolean;
readonly driverId?: string;
readonly error?: string;
export class CreateLeagueUseCase {
constructor(
private readonly leagueRepository: LeagueRepositoryPort,
private readonly output: CreateLeagueOutputPort
) {}
execute(input: CreateLeagueInputPort): void {
// business rules & invariants
const league = League.create(input.name, input.maxMembers);
this.leagueRepository.save(league);
this.output.presentSuccess(league.id);
}
}
export interface CompleteDriverOnboardingOutputPort {
present(result: CompleteDriverOnboardingResult): void;
⸻
5. Ports (Application / Ports)
The Only Two Kinds of Ports
Everything crossing the Application boundary is a Port.
Input Ports
Input Ports describe what a use case needs.
export interface CreateLeagueInputPort {
readonly name: string;
readonly maxMembers: number;
}
The Core does not know or care what happens after present() is called.
Rules:
• Interfaces only
• No behavior
• No validation logic
⸻
2. API Layer (Delivery / Adapter)
Output Ports
API Presenters (Response Mappers)
Output Ports describe how a use case emits outcomes.
API Presenters are Adapters.
export interface CreateLeagueOutputPort {
presentSuccess(leagueId: string): void;
presentFailure(reason: string): void;
}
They:
• implement Core Output Ports
• translate Core Results into API Response DTOs
• store response state temporarily for the controller
Rules:
• No return values
• No getters
• No state
• Use methods, not result objects
They are not View Models.
⸻
6. Application Services (Application / Services)
Definition
An Application Service orchestrates multiple Use Cases.
It exists because:
• No single Use Case should know the whole workflow
• Orchestration is not business logic
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
• Application Services:
• call multiple Use Cases
• define execution order
• handle partial failure & compensation
• Application Services:
• do NOT contain business rules
• do NOT modify entities directly
Naming
• *Presenter or *ResponseMapper
• Output types end with Response or ApiResponse
Structure
core/racing/application/services/
└─ LeagueSetupService.ts
Example (with Edge Cases)
export class LeagueSetupService {
constructor(
private readonly createLeague: CreateLeagueUseCase,
private readonly createSeason: CreateSeasonUseCase,
private readonly assignOwner: AssignLeagueOwnerUseCase,
private readonly notify: SendLeagueWelcomeNotificationUseCase
) {}
execute(input: LeagueSetupInputPort): void {
const leagueId = this.createLeague.execute(input);
try {
this.createSeason.execute({ leagueId });
this.assignOwner.execute({ leagueId, ownerId: input.ownerId });
this.notify.execute({ leagueId, ownerId: input.ownerId });
} catch (error) {
// compensation / rollback logic
throw error;
}
}
}
Edge cases that belong ONLY here:
• Partial failure handling
• Workflow order
• Optional steps
• Retry / idempotency logic
⸻
3. Frontend Layer (apps/website)
7. API Layer (apps/api)
View Models (UI-Owned, Final Form)
Responsibilities
• Transport (HTTP)
• Validation (request shape)
• Mapping to Input Ports
• Calling Application Services
• Adapting Output Ports
A View Model represents fully prepared UI state.
Structure
Only the frontend has Views — therefore only the frontend has View Models.
apps/api/leagues/
├─ LeagueController.ts
├─ presenters/
│ └─ CreateLeaguePresenter.ts
└─ dto/
├─ CreateLeagueRequestDto.ts
└─ CreateLeagueResponseDto.ts
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
Naming
• *ViewModel
⸻
4. Website Presenters (DTO → ViewModel)
API Presenter (Adapter)
export class CreateLeaguePresenter implements CreateLeagueOutputPort {
private response!: CreateLeagueResponseDto;
presentSuccess(leagueId: string): void {
this.response = { success: true, leagueId };
}
presentFailure(reason: string): void {
this.response = { success: false, errorMessage: reason };
}
getResponse(): CreateLeagueResponseDto {
return this.response;
}
}
⸻
8. Frontend Layer (apps/website)
The frontend layer contains UI-specific data shapes. None of these cross into the Core.
There are three distinct frontend data concepts:
1. API DTOs (transport)
2. Command Models (user input / form state)
3. View Models (UI display state)
⸻
8.1 API DTOs (Transport Contracts)
API DTOs represent exact HTTP contracts exposed by the backend.
They are usually generated from OpenAPI or manually mirrored.
apps/website/lib/dtos/
└─ CreateLeagueResponseDto.ts
Rules:
• Exact mirror of backend response
• No UI logic
• No derived values
• Never used directly by components
⸻
8.2 Command Models (User Input / Form State)
Command Models represent user intent before submission.
They are frontend-only and exist to manage:
• form state
• validation feedback
• step-based wizards
They are NOT:
• domain objects
• API DTOs
• View Models
apps/website/lib/commands/
└─ CreateLeagueCommandModel.ts
Rules:
• Classes (stateful)
• May contain client-side validation
• May contain UX-specific helpers (step validation, dirty flags)
• Must expose a method to convert to an API Request DTO
Example responsibility:
• hold incomplete or invalid user input
• guide the user through multi-step flows
• prepare data for submission
Command Models:
• are consumed by components
• are passed into services
• are never sent directly over HTTP
⸻
8.3 View Models (UI Display State)
View Models represent fully prepared UI state after data is loaded.
apps/website/lib/view-models/
└─ CreateLeagueViewModel.ts
Rules:
• Classes only
• UI logic allowed (formatting, labels, derived flags)
• No domain logic
• No mutation after construction
⸻
8.4 Website Presenters (DTO → ViewModel)
Website Presenters are pure mappers.
They:
• convert API Response DTOs into View Models
• perform formatting and reshaping
• are deterministic and side-effect free
export class CreateLeaguePresenter {
present(dto: CreateLeagueResponseDto): CreateLeagueViewModel {
return new CreateLeagueViewModel(dto);
}
}
They are not Core Presenters.
Rules
Rules:
• Input: API DTOs
• Output: View Models
• Must not:
• call APIs
• read storage
• perform decisions
• No side effects
• No API calls
⸻
5. API Client (Frontend)
The API Client is a thin HTTP layer.
Rules
• Sends HTTP requests
• Returns API DTOs only
• Must not:
• return View Models
• contain business logic
• format data for UI
⸻
6. Website Services (Orchestration)
8.5 Website Services (Orchestration)
Website Services orchestrate:
• Command Models
• API Client calls
• Website Presenter mappings
• Presenter mappings
They are the only layer allowed to touch both.
export class LeagueService {
async createLeague(command: CreateLeagueCommandModel): Promise<CreateLeagueViewModel> {
const dto = await this.api.createLeague(command.toRequestDto());
return this.presenter.present(dto);
}
}
Rules
• Services:
• call API Client
• call Website Presenters
• return View Models only
• Components never touch API Client or DTOs
Rules:
• Services accept Command Models
• Services return View Models
• Components never call API clients directly
⸻
7. Final Data Flow (Unambiguous)
View Models (UI State)
Core Use Case
→ OutputPort.present(Result)
apps/website/lib/view-models/
└─ CreateLeagueViewModel.ts
API Presenter (Adapter)
→ maps Result → ApiResponse
export class CreateLeagueViewModel {
constructor(private readonly dto: CreateLeagueResponseDto) {}
API Controller
→ returns ApiResponse (JSON)
get message(): string {
return this.dto.success
? 'League created successfully'
: this.dto.errorMessage ?? 'Creation failed';
}
}
Frontend API Client
→ returns ApiResponse DTO
Rules:
• Classes only
• UI logic allowed
• No domain logic
Website Presenter
→ maps DTO → ViewModel
⸻
Website Presenter (DTO → ViewModel)
export class CreateLeaguePresenter {
present(dto: CreateLeagueResponseDto): CreateLeagueViewModel {
return new CreateLeagueViewModel(dto);
}
}
⸻
Website Service (Orchestration)
export class LeagueService {
constructor(
private readonly api: LeaguesApiClient,
private readonly presenter: CreateLeaguePresenter
) {}
async createLeague(input: unknown): Promise<CreateLeagueViewModel> {
const dto = await this.api.createLeague(input);
return this.presenter.present(dto);
}
}
⸻
9. Full End-to-End Flow (Final)
UI Component
→ consumes ViewModel
→ Website Service
→ API Client
→ HTTP Request DTO
→ API Controller
→ Application Service
→ Use Case(s)
→ Domain
→ Output Port
→ API Presenter
→ HTTP Response DTO
→ Website Presenter
→ View Model
→ UI
⸻
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.
10. Final Non-Negotiable Rules
• Core knows ONLY Ports + Domain
• Core has NO Models, DTOs, or ViewModels
• API talks ONLY to Application Services
• Controllers NEVER call Use Cases directly
• Frontend Components see ONLY View Models
• DTOs never cross into UI components
⸻
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
11. Final Merksatz
⸻
Use Cases decide.
Application Services orchestrate.
Adapters translate.
UI presents.
10. Final Merksatz
The Core emits results.
The API transports them.
The Frontend interprets them.
If a type tries to do more than one of these — it is incorrectly placed.
If a class violates more than one of these roles, it is incorrectly placed.
+115
View File
@@ -0,0 +1,115 @@
# File Structure
## Core
```
core/ # * Business- & Anwendungslogik (framework-frei)
├── shared/ # * Gemeinsame Core-Bausteine
│ ├── domain/ # * Domain-Basistypen
│ │ ├── Entity.ts # * Basisklasse für Entities
│ │ ├── ValueObject.ts # * Basisklasse für Value Objects
│ │ └── DomainError.ts # * Domain-spezifische Fehler
│ └── application/
│ └── ApplicationError.ts # * Use-Case-/Application-Fehler
│
├── racing/ # * Beispiel-Domain (Bounded Context)
│ ├── domain/ # * Fachliche Wahrheit
│ │ ├── entities/ # * Aggregate Roots & Entities
│ │ │ ├── League.ts # * Aggregate Root
│ │ │ └── Race.ts # * Entity
│ │ ├── value-objects/ # * Unveränderliche Fachwerte
│ │ │ └── LeagueName.ts # * Beispiel VO
│ │ ├── services/ # * Domain Services (Regeln, kein Ablauf)
│ │ │ └── ChampionshipScoringService.ts # * Regel über mehrere Entities
│ │ └── errors/ # * Domain-Invariantenfehler
│ │ └── RacingDomainError.ts
│ │
│ └── application/ # * Anwendungslogik
│ ├── ports/ # * EINZIGE Schnittstellen des Cores
│ │ ├── input/ # * Input Ports (Use-Case-Grenzen)
│ │ │ └── CreateLeagueInputPort.ts
│ │ └── output/ # * Output Ports (Use-Case-Ergebnisse)
│ │ └── CreateLeagueOutputPort.ts
│ │
│ ├── use-cases/ # * Einzelne Business-Intents
│ │ └── CreateLeagueUseCase.ts
│ │
│ └── services/ # * Application Services (Orchestrierung)
│ └── LeagueSetupService.ts # * Koordiniert mehrere Use Cases
```
## Adapters
```
adapters/ # * Alle äußeren Implementierungen
├── persistence/ # * Datenhaltung
│ ├── typeorm/ # Konkrete DB-Technologie
│ │ ├── entities/ # ORM-Entities (nicht Domain!)
│ │ └── repositories/ # * Implementieren Core-Ports
│ │ └── LeagueRepository.ts
│ └── inmemory/ # Test-/Dev-Implementierungen
│ └── LeagueRepository.ts
│
├── notifications/ # Externe Systeme
│ └── EmailNotificationAdapter.ts # Implementiert Notification-Port
│
├── logging/ # Logging / Telemetrie
│ └── ConsoleLoggerAdapter.ts # Adapter für Logger-Port
│
└── bootstrap/ # Initialisierung / Seeding
└── EnsureInitialData.ts # App-Start-Logik
```
## API
```
apps/api/ # * Delivery Layer (HTTP)
├── app.module.ts # * Framework-Zusammenbau
│
├── leagues/ # * Feature-Modul
│ ├── LeagueController.ts # * HTTP → Application Service
│ │
│ ├── dto/ # * Transport-DTOs (HTTP)
│ │ ├── CreateLeagueRequestDto.ts # * Request-Shape
│ │ └── CreateLeagueResponseDto.ts # * Response-Shape
│ │
│ └── presenters/ # * Output-Port-Adapter
│ └── CreateLeaguePresenter.ts # * Core Output → HTTP Response
│
└── shared/ # API-spezifisch
└── filters/ # Exception-Handling
```
## Frontend
```
apps/website/ # * Frontend (UI)
├── app/ # * Next.js Routen
│ └── leagues/ # * Page-Level
│ └── page.tsx
│
├── components/ # * Reine UI-Komponenten
│ └── LeagueForm.tsx
│
├── lib/
│ ├── api/ # * HTTP-Client
│ │ └── LeaguesApiClient.ts # * Gibt NUR API DTOs zurück
│ │
│ ├── dtos/ # * API-Vertrags-Typen
│ │ └── CreateLeagueResponseDto.ts
│ │
│ ├── commands/ # * Command Models (Form State)
│ │ └── CreateLeagueCommandModel.ts
│ │
│ ├── presenters/ # * DTO → ViewModel Mapper
│ │ └── CreateLeaguePresenter.ts
│ │
│ ├── view-models/ # * UI-State
│ │ └── CreateLeagueViewModel.ts
│ │
│ ├── services/ # * Frontend-Orchestrierung
│ │ └── LeagueService.ts
│ │
│ └── blockers/ # UX-Schutz (Throttle, Submit)
│ ├── SubmitBlocker.ts
│ └── ThrottleBlocker.ts
```