docs
This commit is contained in:
@@ -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')."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
docs/architecture/FILE_STRUCTURE.md
Normal file
115
docs/architecture/FILE_STRUCTURE.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user