# Website Architecture Refactoring Plan (CORRECTED) ## Executive Summary I've identified **75+ violations** where `apps/website` directly imports from core domain, use cases, or repositories. This plan provides the **CORRECT** architecture with: - **One presenter = One transformation = One `present()` method** - **Pure constructor injection** - **Stateless presenters** - **No singleton exports** - **No redundant index.ts files** --- ## ✅ Correct Presenter Architecture ### The Rule: One Presenter = One Transformation Each presenter has **exactly one responsibility**: transform one specific DTO into one specific ViewModel. ```typescript // ✅ CORRECT - Single present() method, one purpose export class RaceDetailPresenter { present(dto: RaceDetailDto): RaceDetailViewModel { return new RaceDetailViewModel(dto); } } // ✅ CORRECT - Another presenter for different transformation export class RaceResultsDetailPresenter { present(dto: RaceResultsDetailDto, currentUserId?: string): RaceResultsDetailViewModel { return new RaceResultsDetailViewModel(dto, currentUserId); } } // ✅ CORRECT - Yet another presenter for different transformation export class RaceWithSOFPresenter { present(dto: RaceWithSOFDto): RaceWithSOFViewModel { return { id: dto.raceId, strengthOfField: dto.strengthOfField, registeredCount: dto.registeredCount, // ... pure transformation }; } } ``` --- ## 🏗️ Correct Service Layer Design ### Service with Multiple Focused Presenters ```typescript // ✅ apps/website/lib/services/races/RaceService.ts import { RacesApiClient } from '@/lib/api/races/RacesApiClient'; import { RaceDetailPresenter } from '@/lib/presenters/RaceDetailPresenter'; import { RacesPagePresenter } from '@/lib/presenters/RacesPagePresenter'; import type { RaceDetailViewModel } from '@/lib/view-models/RaceDetailViewModel'; import type { RacesPageViewModel } from '@/lib/view-models/RacesPageViewModel'; /** * Race Service * * Orchestrates race operations. Each operation uses its own focused presenter * for a specific DTO-to-ViewModel transformation. */ export class RaceService { constructor( private readonly apiClient: RacesApiClient, private readonly raceDetailPresenter: RaceDetailPresenter, private readonly racesPagePresenter: RacesPagePresenter ) {} async getRaceDetail(raceId: string, driverId: string): Promise { const dto = await this.apiClient.getDetail(raceId, driverId); return this.raceDetailPresenter.present(dto); } async getRacesPageData(): Promise { const dto = await this.apiClient.getPageData(); return this.racesPagePresenter.present(dto); } async completeRace(raceId: string): Promise { await this.apiClient.complete(raceId); } } ``` ### Race Results Service with Multiple Presenters ```typescript // ✅ apps/website/lib/services/races/RaceResultsService.ts import { RacesApiClient } from '@/lib/api/races/RacesApiClient'; import { RaceResultsDetailPresenter } from '@/lib/presenters/RaceResultsDetailPresenter'; import { RaceWithSOFPresenter } from '@/lib/presenters/RaceWithSOFPresenter'; import { ImportRaceResultsSummaryPresenter } from '@/lib/presenters/ImportRaceResultsSummaryPresenter'; import type { RaceResultsDetailViewModel } from '@/lib/view-models/RaceResultsDetailViewModel'; import type { RaceWithSOFViewModel } from '@/lib/view-models/RaceWithSOFViewModel'; import type { ImportRaceResultsSummaryViewModel } from '@/lib/view-models/ImportRaceResultsSummaryViewModel'; export class RaceResultsService { constructor( private readonly apiClient: RacesApiClient, private readonly resultsDetailPresenter: RaceResultsDetailPresenter, private readonly sofPresenter: RaceWithSOFPresenter, private readonly importSummaryPresenter: ImportRaceResultsSummaryPresenter ) {} async getResultsDetail(raceId: string, currentUserId?: string): Promise { const dto = await this.apiClient.getResultsDetail(raceId); return this.resultsDetailPresenter.present(dto, currentUserId); } async getWithSOF(raceId: string): Promise { const dto = await this.apiClient.getWithSOF(raceId); return this.sofPresenter.present(dto); } async importResults(raceId: string, input: any): Promise { const dto = await this.apiClient.importResults(raceId, input); return this.importSummaryPresenter.present(dto); } } ``` --- ## 📁 Correct Presenter Organization ``` apps/website/lib/presenters/ ├── RaceDetailPresenter.ts # RaceDetailDto -> RaceDetailViewModel ├── RacesPagePresenter.ts # RacesPageDataDto -> RacesPageViewModel ├── RaceResultsDetailPresenter.ts # RaceResultsDetailDto -> RaceResultsDetailViewModel ├── RaceWithSOFPresenter.ts # RaceWithSOFDto -> RaceWithSOFViewModel ├── ImportRaceResultsSummaryPresenter.ts # ImportRaceResultsSummaryDto -> ImportRaceResultsSummaryViewModel ├── LeagueDetailPresenter.ts # LeagueDetailDto -> LeagueDetailViewModel ├── LeagueStandingsPresenter.ts # LeagueStandingsDto -> LeagueStandingsViewModel ├── LeagueStatsPresenter.ts # LeagueStatsDto -> LeagueStatsViewModel ├── DriverProfilePresenter.ts # DriverProfileDto -> DriverProfileViewModel ├── DriverLeaderboardPresenter.ts # DriverLeaderboardDto -> DriverLeaderboardViewModel ├── TeamDetailsPresenter.ts # TeamDetailsDto -> TeamDetailsViewModel ├── TeamMembersPresenter.ts # TeamMembersDto -> TeamMembersViewModel └── ... NO index.ts files NO multi-method presenters ``` --- ## 🏗️ Correct ServiceFactory ```typescript // ✅ apps/website/lib/services/ServiceFactory.ts import { ApiClient } from '@/lib/api'; import { RaceService } from '@/lib/services/races/RaceService'; import { RaceResultsService } from '@/lib/services/races/RaceResultsService'; import { LeagueService } from '@/lib/services/leagues/LeagueService'; import { DriverService } from '@/lib/services/drivers/DriverService'; import { TeamService } from '@/lib/services/teams/TeamService'; // Race presenters import { RaceDetailPresenter } from '@/lib/presenters/RaceDetailPresenter'; import { RacesPagePresenter } from '@/lib/presenters/RacesPagePresenter'; import { RaceResultsDetailPresenter } from '@/lib/presenters/RaceResultsDetailPresenter'; import { RaceWithSOFPresenter } from '@/lib/presenters/RaceWithSOFPresenter'; import { ImportRaceResultsSummaryPresenter } from '@/lib/presenters/ImportRaceResultsSummaryPresenter'; // League presenters import { LeagueDetailPresenter } from '@/lib/presenters/LeagueDetailPresenter'; import { LeagueStandingsPresenter } from '@/lib/presenters/LeagueStandingsPresenter'; import { LeagueStatsPresenter } from '@/lib/presenters/LeagueStatsPresenter'; // Driver presenters import { DriverProfilePresenter } from '@/lib/presenters/DriverProfilePresenter'; import { DriverLeaderboardPresenter } from '@/lib/presenters/DriverLeaderboardPresenter'; // Team presenters import { TeamDetailsPresenter } from '@/lib/presenters/TeamDetailsPresenter'; import { TeamMembersPresenter } from '@/lib/presenters/TeamMembersPresenter'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; /** * Service Factory - Composition Root * * Creates and wires up all services with their dependencies. * Each service gets all the presenters it needs. */ export class ServiceFactory { private static apiClient = new ApiClient(API_BASE_URL); // Race presenters private static raceDetailPresenter = new RaceDetailPresenter(); private static racesPagePresenter = new RacesPagePresenter(); private static raceResultsDetailPresenter = new RaceResultsDetailPresenter(); private static raceWithSOFPresenter = new RaceWithSOFPresenter(); private static importRaceResultsSummaryPresenter = new ImportRaceResultsSummaryPresenter(); // League presenters private static leagueDetailPresenter = new LeagueDetailPresenter(); private static leagueStandingsPresenter = new LeagueStandingsPresenter(); private static leagueStatsPresenter = new LeagueStatsPresenter(); // Driver presenters private static driverProfilePresenter = new DriverProfilePresenter(); private static driverLeaderboardPresenter = new DriverLeaderboardPresenter(); // Team presenters private static teamDetailsPresenter = new TeamDetailsPresenter(); private static teamMembersPresenter = new TeamMembersPresenter(); static createRaceService(): RaceService { return new RaceService( this.apiClient.races, this.raceDetailPresenter, this.racesPagePresenter ); } static createRaceResultsService(): RaceResultsService { return new RaceResultsService( this.apiClient.races, this.raceResultsDetailPresenter, this.raceWithSOFPresenter, this.importRaceResultsSummaryPresenter ); } static createLeagueService(): LeagueService { return new LeagueService( this.apiClient.leagues, this.leagueDetailPresenter, this.leagueStandingsPresenter, this.leagueStatsPresenter ); } static createDriverService(): DriverService { return new DriverService( this.apiClient.drivers, this.driverProfilePresenter, this.driverLeaderboardPresenter ); } static createTeamService(): TeamService { return new TeamService( this.apiClient.teams, this.teamDetailsPresenter, this.teamMembersPresenter ); } } ``` --- ## ✅ Complete Example: Race Domain ### File Structure ``` apps/website/lib/ ├── services/races/ │ ├── RaceService.ts │ └── RaceResultsService.ts ├── presenters/ │ ├── RaceDetailPresenter.ts │ ├── RacesPagePresenter.ts │ ├── RaceResultsDetailPresenter.ts │ ├── RaceWithSOFPresenter.ts │ └── ImportRaceResultsSummaryPresenter.ts ├── view-models/ │ ├── RaceDetailViewModel.ts │ ├── RacesPageViewModel.ts │ ├── RaceResultsDetailViewModel.ts │ ├── RaceWithSOFViewModel.ts │ └── ImportRaceResultsSummaryViewModel.ts └── dtos/ ├── RaceDetailDto.ts ├── RacesPageDataDto.ts ├── RaceResultsDetailDto.ts ├── RaceWithSOFDto.ts └── ImportRaceResultsSummaryDto.ts ``` --- ## 🎯 Key Architectural Principles 1. **One Presenter = One Transformation** - Each presenter has exactly one `present()` method 2. **Service = Orchestrator** - Services coordinate API calls and presenter transformations 3. **Multiple Presenters per Service** - Services inject all presenters they need 4. **No Presenter Reuse Across Domains** - Each domain has its own presenters 5. **ServiceFactory = Composition Root** - Single place to wire everything up 6. **Stateless Presenters** - No instance state, pure transformations 7. **Constructor Injection** - All dependencies explicit --- ## 📋 Migration Checklist ### For Each Presenter: - [ ] Verify exactly one `present()` method - [ ] Verify stateless (no instance properties) - [ ] Verify pure transformation (no side effects) - [ ] Remove any functional wrapper exports ### For Each Service: - [ ] Identify all DTO-to-ViewModel transformations needed - [ ] Inject all required presenters via constructor - [ ] Each method calls appropriate presenter - [ ] No presenter logic in service ### For ServiceFactory: - [ ] Create shared presenter instances - [ ] Wire presenters to services via constructor - [ ] One factory method per service --- ## ✅ Success Criteria 1. ✅ Each presenter has exactly one `present()` method 2. ✅ Services inject all presenters they need 3. ✅ No multi-method presenters 4. ✅ ServiceFactory wires everything correctly 5. ✅ Zero direct imports from `@core` 6. ✅ All tests pass