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

View File

@@ -91,6 +91,10 @@
{ {
"selector": "ExportDefaultDeclaration", "selector": "ExportDefaultDeclaration",
"message": "Default exports are forbidden. Use named exports instead." "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')."
} }
] ]
} }

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. It explicitly covers:
There is no overlap of terminology across layers. • 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 Rules
Core Output Ports: A Use Case:
define what data is emitted contains business logic
do not store state enforces invariants
do not expose getters operates on domain entities
do not reference DTOs or View Models communicates ONLY via ports
Core never pulls data back from an output port A Use Case:
Core calls present() and stops does NOT orchestrate multiple workflows
• does NOT know HTTP, UI, DB, queues
Naming Structure
• *OutputPort
• *Result (pure application result) core/racing/application/use-cases/
└─ CreateLeagueUseCase.ts
Example Example
export interface CompleteDriverOnboardingResult { export class CreateLeagueUseCase {
readonly success: boolean; constructor(
readonly driverId?: string; private readonly leagueRepository: LeagueRepositoryPort,
readonly error?: string; 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: Rules:
implement Core Output Ports No return values
translate Core Results into API Response DTOs No getters
store response state temporarily for the controller 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 Rules
• API Presenters: • Application Services:
implement a Core Output Port call multiple Use Cases
map Core Results → API Responses define execution order
may store state internally handle partial failure & compensation
• API Presenters must not: • Application Services:
• contain business logic do NOT contain business rules
reference frontend View Models do NOT modify entities directly
Naming Structure
• *Presenter or *ResponseMapper
• Output types end with Response or ApiResponse 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. Website Presenters are pure mappers.
They: export class CreateLeaguePresenter {
• convert API Response DTOs into View Models present(dto: CreateLeagueResponseDto): CreateLeagueViewModel {
• perform formatting and reshaping return new CreateLeagueViewModel(dto);
• are deterministic and side-effect free }
}
They are not Core Presenters. Rules:
Rules
• Input: API DTOs • Input: API DTOs
• Output: View Models • Output: View Models
Must not: No side effects
• call APIs No API calls
• read storage
• perform decisions
5. API Client (Frontend) 8.5 Website Services (Orchestration)
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)
Website Services orchestrate: Website Services orchestrate:
• Command Models
• API Client calls • 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 Rules:
• Services: • Services accept Command Models
call API Client Services return View Models
call Website Presenters Components never call API clients directly
• return View Models only
• Components never touch API Client or DTOs
7. Final Data Flow (Unambiguous) View Models (UI State)
Core Use Case apps/website/lib/view-models/
→ OutputPort.present(Result) └─ CreateLeagueViewModel.ts
API Presenter (Adapter) export class CreateLeagueViewModel {
→ maps Result → ApiResponse constructor(private readonly dto: CreateLeagueResponseDto) {}
API Controller get message(): string {
returns ApiResponse (JSON) return this.dto.success
? 'League created successfully'
: this.dto.errorMessage ?? 'Creation failed';
}
}
Frontend API Client Rules:
→ returns ApiResponse DTO • 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 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) 10. Final Non-Negotiable Rules
• Core knows ONLY Ports + Domain
Term Layer Meaning • Core has NO Models, DTOs, or ViewModels
OutputPort Core Use case output contract • API talks ONLY to Application Services
Result Core Pure application result • Controllers NEVER call Use Cases directly
Presenter (API) apps/api Maps Result → API Response • Frontend Components see ONLY View Models
Response / ApiResponse apps/api HTTP transport shape • DTOs never cross into UI components
Presenter (Website) apps/website Maps DTO → ViewModel
ViewModel apps/website UI-ready state
No term is reused with a different meaning.
9. Non-Negotiable Rules 11. Final Merksatz
• 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
Use Cases decide.
Application Services orchestrate.
Adapters translate.
UI presents.
10. Final Merksatz If a class violates more than one of these roles, it is incorrectly placed.
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.

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
```