# Website Services Architecture This document defines the role and responsibilities of services in the website layer (`apps/website/lib/services/`). ## Overview Website services are **frontend orchestration services**. They bridge the gap between server-side composition (PageQueries, Server Actions) and API infrastructure. ## Purpose Website services answer: **"How does the website orchestrate API calls and handle infrastructure?"** ## Responsibilities ### ✅ Services MAY: - Call API clients - Orchestrate multiple API calls - Handle infrastructure concerns (logging, error reporting, retries) - Transform API DTOs to Page DTOs (if orchestration is needed) - Cache responses (in-memory, request-scoped) - Handle recoverable errors ### ❌ Services MUST NOT: - Contain business rules (that's for core use cases) - Create ViewModels (ViewModels are client-only) - Import from `lib/view-models/` or `templates/` - Perform UI rendering logic - Store state across requests ## Placement ``` apps/website/lib/services/ ``` ## Pattern ### Service Definition Services create their own dependencies: ```typescript import { AdminApiClient } from '@/lib/api/admin/AdminApiClient'; import type { UserDto } from '@/lib/api/admin/AdminApiClient'; import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter'; export class AdminService { private apiClient: AdminApiClient; constructor() { // Service creates its own dependencies const baseUrl = process.env.NEXT_PUBLIC_API_URL || ''; this.apiClient = new AdminApiClient( baseUrl, new ConsoleErrorReporter(), new ConsoleLogger() ); } async updateUserStatus(userId: string, status: string): Promise> { try { const result = await this.apiClient.updateUserStatus(userId, status); return Result.ok(result); } catch (error) { // Convert HTTP errors to domain errors if (error instanceof HttpForbiddenError) { return Result.err(new ForbiddenError('Insufficient permissions')); } return Result.err(new UnknownError('Failed to update user')); } } } ``` ### Usage in PageQueries (Reads) ```typescript // apps/website/lib/page-queries/AdminDashboardPageQuery.ts import { AdminService } from '@/lib/services/admin/AdminService'; import { AdminDashboardViewDataBuilder } from '@/lib/builders/view-data/AdminDashboardViewDataBuilder'; export class AdminDashboardPageQuery implements PageQuery { async execute(): Promise> { // Manual construction: Service creates its own dependencies const service = new AdminService(); // Use service const result = await service.getDashboardStats(); if (result.isErr()) { return Result.err(mapToPresentationError(result.error)); } // Transform to ViewData using Builder const viewData = AdminDashboardViewDataBuilder.build(result.value); return Result.ok(viewData); } } ``` ### Usage in Mutations (Writes) ```typescript // apps/website/lib/mutations/UpdateUserStatusMutation.ts import { AdminService } from '@/lib/services/admin/AdminService'; export class UpdateUserStatusMutation implements Mutation { async execute(input: UpdateUserStatusInput): Promise> { // Manual construction: Service creates its own dependencies const service = new AdminService(); const result = await service.updateUserStatus(input.userId, input.status); if (result.isErr()) { return Result.err(mapToMutationError(result.error)); } return Result.ok(undefined); } } ``` ### Usage in Server Actions ```typescript // app/admin/actions.ts 'use server'; import { UpdateUserStatusMutation } from '@/lib/mutations/UpdateUserStatusMutation'; import { revalidatePath } from 'next/cache'; export async function updateUserStatus(input: UpdateUserStatusInput) { // Manual construction: Mutation creates Service const mutation = new UpdateUserStatusMutation(); const result = await mutation.execute(input); if (result.isErr()) { return { success: false, error: result.error }; } revalidatePath('/admin/users'); return { success: true }; } ``` ## Dependency Chain ``` RSC Page / Server Action ↓ (manual construction) PageQuery / Mutation ↓ (manual construction) Service ↓ (creates own dependencies) API Client ↓ (makes HTTP calls) API ``` **Key Principle**: Each layer manually constructs the next layer. Services create their own infrastructure (API Client, Logger, ErrorReporter). ## Why Manual Construction? **See**: [DEPENDENCY_CONSTRUCTION.md](./DEPENDENCY_CONSTRUCTION.md) **Summary**: - ✅ Explicit and clear - ✅ No singleton issues - ✅ No request-scoping problems - ✅ Easy to test (pass mocks to constructor) - ✅ Works perfectly with Next.js RSC - ❌ No DI container needed ## Naming - Service classes: `*Service` - Service methods: Return `Result` - Variable names: `apiDto`, `viewData` (never just `dto`) ## Comparison with Other Layers | Layer | Purpose | Example | |-------|---------|---------| | **Website Service** | Orchestrate API calls, handle errors | `AdminService` | | **API Client** | HTTP infrastructure | `AdminApiClient` | | **Core Use Case** | Business rules | `CreateLeagueUseCase` | | **Domain Service** | Cross-entity logic | `StrengthOfFieldCalculator` | ## Anti-Patterns ❌ **Wrong**: Service creates ViewModels ```typescript // WRONG class AdminService { async getUser(userId: string): Promise { const dto = await this.apiClient.getUser(userId); return new UserViewModel(dto); // ❌ ViewModels are client-only } } ``` ✅ **Correct**: Service returns DTOs ```typescript // CORRECT class AdminService { async getUser(userId: string): Promise> { try { const dto = await this.apiClient.getUser(userId); return Result.ok(dto); // ✅ DTOs are fine } catch (error) { return Result.err(new NotFoundError('User not found')); } } } ``` ❌ **Wrong**: Service contains business logic ```typescript // WRONG class AdminService { async canDeleteUser(userId: string): Promise { const user = await this.apiClient.getUser(userId); return user.role !== 'admin'; // ❌ Business rule belongs in core } } ``` ✅ **Correct**: Service orchestrates ```typescript // CORRECT class AdminService { async getUser(userId: string): Promise> { try { const dto = await this.apiClient.getUser(userId); return Result.ok(dto); } catch (error) { return Result.err(new NotFoundError('User not found')); } } } // Business logic in core use case or page query ``` ❌ **Wrong**: Server action calls API client directly ```typescript // WRONG 'use server'; export async function updateUserStatus(userId: string, status: string) { const apiClient = new AdminApiClient(...); await apiClient.updateUserStatus(userId, status); // ❌ Should use service } ``` ✅ **Correct**: Server action uses Mutation ```typescript // CORRECT 'use server'; export async function updateUserStatus(input: UpdateUserStatusInput) { const mutation = new UpdateUserStatusMutation(); const result = await mutation.execute(input); // ... } ``` ❌ **Wrong**: PageQuery creates API Client ```typescript // WRONG export class DashboardPageQuery { async execute() { const apiClient = new DashboardApiClient(...); // ❌ Should use Service return await apiClient.getOverview(); } } ``` ✅ **Correct**: PageQuery uses Service ```typescript // CORRECT export class DashboardPageQuery { async execute() { const service = new DashboardService(); // ✅ Service creates API Client return await service.getDashboardOverview(); } } ``` ## Summary Website services are **thin orchestration wrappers** that create their own dependencies and handle error conversion. **Key principles**: 1. ✅ Services create their own dependencies (API Client, Logger, ErrorReporter) 2. ✅ Services return `Result` 3. ✅ Services convert HTTP errors to Domain errors 4. ❌ Services don't create ViewModels 5. ❌ Services don't contain business rules 6. ✅ PageQueries/Mutations use Services, not API Clients directly 7. ✅ Manual construction (no DI container in RSC)