Files
gridpilot.gg/docs/architecture/website/SERVICES.md
2026-01-12 19:24:59 +01:00

6.8 KiB

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

import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
import type { UserDto } from '@/lib/api/admin/AdminApiClient';

export class AdminService {
  constructor(private readonly apiClient: AdminApiClient) {}

  async updateUserStatus(userId: string, status: string): Promise<UserDto> {
    return this.apiClient.updateUserStatus(userId, status);
  }
}

Usage in PageQueries (Reads)

// apps/website/lib/page-queries/AdminDashboardPageQuery.ts
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
import { AdminService } from '@/lib/services/admin/AdminService';

export class AdminDashboardPageQuery {
  async execute(): Promise<PageQueryResult<AdminDashboardPageDto>> {
    // Create infrastructure
    const logger = new ConsoleLogger();
    const errorReporter = new EnhancedErrorReporter(logger, {...});
    const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
    const apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
    const service = new AdminService(apiClient);
    
    // Use service
    const stats = await service.getDashboardStats();
    
    // Transform to Page DTO
    return { status: 'ok', dto: transformToPageDto(stats) };
  }
}

Usage in Server Actions (Writes)

// apps/website/app/admin/actions.ts
'use server';

import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';
import { AdminService } from '@/lib/services/admin/AdminService';
import { revalidatePath } from 'next/cache';

export async function updateUserStatus(userId: string, status: string): Promise<void> {
  try {
    // Create infrastructure
    const logger = new ConsoleLogger();
    const errorReporter = new EnhancedErrorReporter(logger, {
      showUserNotifications: true,
      logToConsole: true,
      reportToExternal: process.env.NODE_ENV === 'production',
    });
    
    const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
    const apiClient = new AdminApiClient(baseUrl, errorReporter, logger);
    const service = new AdminService(apiClient);
    
    // Use service (NOT API client directly)
    await service.updateUserStatus(userId, status);
    
    // Revalidate
    revalidatePath('/admin/users');
  } catch (error) {
    console.error('updateUserStatus failed:', error);
    throw new Error('Failed to update user status');
  }
}

Infrastructure Concerns

Where should logging/error reporting live?

In the current architecture, server actions and PageQueries create infrastructure. This is acceptable because:

  1. Next.js serverless functions are stateless
  2. Each request needs fresh infrastructure
  3. Manual DI is clearer than magic containers

Key principle: Services orchestrate, they don't create infrastructure.

Dependency Chain

Server Action / PageQuery
  ↓ (creates infrastructure)
Service
  ↓ (orchestrates)
API Client
  ↓ (makes HTTP calls)
API

Naming

  • Service classes: *Service
  • Service methods: Return DTOs (not ViewModels)
  • Variable names: apiDto, pageDto (never just dto)

Comparison with Other Layers

Layer Purpose Example
Website Service Orchestrate API calls AdminService
API Client HTTP infrastructure AdminApiClient
Core Use Case Business rules CreateLeagueUseCase
Domain Service Cross-entity logic StrengthOfFieldCalculator

Anti-Patterns

Wrong: Service creates ViewModels

// WRONG
class AdminService {
  async getUser(userId: string): Promise<UserViewModel> {
    const dto = await this.apiClient.getUser(userId);
    return new UserViewModel(dto); // ❌ ViewModels are client-only
  }
}

Correct: Service returns DTOs

// CORRECT
class AdminService {
  async getUser(userId: string): Promise<UserDto> {
    return this.apiClient.getUser(userId); // ✅ DTOs are fine
  }
}

Wrong: Service contains business logic

// WRONG
class AdminService {
  async canDeleteUser(userId: string): Promise<boolean> {
    const user = await this.apiClient.getUser(userId);
    return user.role !== 'admin'; // ❌ Business rule belongs in core
  }
}

Correct: Service orchestrates

// CORRECT
class AdminService {
  async getUser(userId: string): Promise<UserDto> {
    return this.apiClient.getUser(userId);
  }
}
// Business logic in core use case or page query

Wrong: Server action calls API client directly

// 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 service

// CORRECT
'use server';
export async function updateUserStatus(userId: string, status: string) {
  const apiClient = new AdminApiClient(...);
  const service = new AdminService(apiClient);
  await service.updateUserStatus(userId, status); // ✅ Uses service
}

Summary

Website services are thin orchestration wrappers around API clients. They handle infrastructure concerns so that PageQueries and Server Actions can focus on composition and validation.

Key principles:

  1. Services orchestrate API calls
  2. Server actions/PageQueries create infrastructure
  3. Services don't create ViewModels
  4. Services don't contain business rules
  5. Server actions MUST use services, not API clients directly