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

@@ -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.

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