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/ortemplates/ - 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:
- Next.js serverless functions are stateless
- Each request needs fresh infrastructure
- 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 justdto)
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:
- Services orchestrate API calls
- Server actions/PageQueries create infrastructure
- Services don't create ViewModels
- Services don't contain business rules
- Server actions MUST use services, not API clients directly