# PageQuery Error Handling Design (Option 1: Clean Architecture)

## The Problem

The current `DashboardPageQuery` mixes HTTP concerns with domain logic:

```typescript
// ❌ WRONG - Mixes HTTP status codes with PageQuery logic
interface ErrorWithStatusCode extends Error {
  statusCode?: number;
}

if (errorWithStatus.statusCode === 404) {
  return Result.err('notFound');
}
```

This violates Clean Architecture because:
- PageQueries should not know about HTTP status codes
- PageQueries should use Services, not API Clients directly
- Error classification should happen at the Service layer
- PageQueries should only deal with domain error IDs

## The Solution: Layered Error Handling

### Layer 1: API Client (HTTP → Technical Errors)

The API client handles HTTP communication and catches HTTP errors:

```typescript
// apps/website/lib/api/dashboard/DashboardApiClient.ts
export class DashboardApiClient {
  async getDashboardOverview(): Promise<DashboardStats> {
    const response = await fetch(`${this.baseUrl}/dashboard/overview`, {
      headers: this.getHeaders(),
    });

    if (!response.ok) {
      // Throw HTTP-specific errors (will be caught by Service)
      if (response.status === 404) {
        throw new HttpNotFoundError('Dashboard not found');
      }
      if (response.status === 401 || response.status === 403) {
        throw new HttpUnauthorizedError('Access denied');
      }
      if (response.status >= 500) {
        throw new HttpServerError('Server error');
      }
      throw new HttpError(`HTTP ${response.status}`);
    }

    return response.json();
  }
}

// HTTP error classes
class HttpError extends Error {}
class HttpNotFoundError extends HttpError {}
class HttpUnauthorizedError extends HttpError {}
class HttpServerError extends HttpError {}
```

### Layer 2: Service (Technical → Domain Errors)

The Service creates its own dependencies and converts HTTP errors to domain errors.

**See**: [DEPENDENCY_CONSTRUCTION.md](./DEPENDENCY_CONSTRUCTION.md) for why Services create their own dependencies.

```typescript
// apps/website/lib/services/dashboard/DashboardService.ts
import { DashboardApiClient } from '@/lib/api/dashboard/DashboardApiClient';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';

export type DashboardServiceError =
  | { type: 'notFound'; message: string }
  | { type: 'unauthorized'; message: string }
  | { type: 'serverError'; message: string }
  | { type: 'networkError'; message: string }
  | { type: 'unknown'; message: string };

export class DashboardService {
  private apiClient: DashboardApiClient;

  constructor() {
    // Service creates its own dependencies
    const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
    this.apiClient = new DashboardApiClient(
      baseUrl,
      new ConsoleErrorReporter(),
      new ConsoleLogger()
    );
  }

  async getDashboardOverview(): Promise<Result<DashboardStats, DashboardServiceError>> {
    try {
      const data = await this.apiClient.getDashboardOverview();
      return Result.ok(data);
    } catch (error) {
      // Convert HTTP errors to domain errors
      if (error instanceof HttpNotFoundError) {
        return Result.err({ type: 'notFound', message: error.message });
      }
      if (error instanceof HttpUnauthorizedError) {
        return Result.err({ type: 'unauthorized', message: error.message });
      }
      if (error instanceof HttpServerError) {
        return Result.err({ type: 'serverError', message: error.message });
      }
      if (error instanceof HttpError) {
        return Result.err({ type: 'unknown', message: error.message });
      }
      if (error instanceof Error) {
        return Result.err({ type: 'networkError', message: error.message });
      }
      return Result.err({ type: 'unknown', message: 'Unknown error' });
    }
  }
}
```

**Key Points:**
- ✅ Service creates its own API Client
- ✅ Service creates its own Logger and ErrorReporter
- ✅ Catches HTTP errors and converts to domain errors
- ✅ Returns Result type

### Layer 3: PageQuery (Domain → Presentation Errors)

PageQueries use Services and map domain errors to presentation errors.

**See**: [DEPENDENCY_CONSTRUCTION.md](./DEPENDENCY_CONSTRUCTION.md) for why we use manual construction.

```typescript
// apps/website/lib/page-queries/page-queries/DashboardPageQuery.ts
import { DashboardService, type DashboardServiceError } from '@/lib/services/dashboard/DashboardService';

// Presentation error IDs (what the template/redirect logic understands)
type DashboardPageError = 'notFound' | 'redirect' | 'DASHBOARD_FETCH_FAILED' | 'UNKNOWN_ERROR';

export class DashboardPageQuery implements PageQuery<DashboardViewData, void> {
  async execute(): Promise<Result<DashboardViewData, DashboardPageError>> {
    // Manual construction: Service creates its own dependencies
    const dashboardService = new DashboardService();
    
    // Call service
    const serviceResult = await dashboardService.getDashboardOverview();
    
    if (serviceResult.isErr()) {
      const serviceError = serviceResult.error;
      
      // Map service errors to presentation errors
      switch (serviceError.type) {
        case 'notFound':
          return Result.err('notFound');
        case 'unauthorized':
          return Result.err('redirect'); // Redirect to login
        case 'serverError':
        case 'networkError':
        case 'unknown':
          return Result.err('DASHBOARD_FETCH_FAILED');
        default:
          return Result.err('UNKNOWN_ERROR');
      }
    }
    
    // Success - transform to ViewData
    const apiDto = serviceResult.value;
    const viewData = DashboardViewDataBuilder.build(apiDto);
    return Result.ok(viewData);
  }
}
```

**Key Points:**
- ✅ PageQuery constructs only the Service
- ✅ Service handles its own dependencies (API Client, Logger, etc.)
- ❌ No API Client instantiation in PageQuery
- ✅ Map domain errors to presentation errors
- ✅ Transform API DTO to ViewData using Builder

### Layer 4: RSC Page (Presentation → User)

The RSC page handles presentation errors:

```typescript
// apps/website/app/dashboard/page.tsx
import { DashboardPageQuery } from '@/lib/page-queries/page-queries/DashboardPageQuery';
import { DashboardTemplate } from '@/templates/DashboardTemplate';

export default async function DashboardPage() {
  const query = new DashboardPageQuery();
  const result = await query.execute();

  if (result.isErr()) {
    switch (result.error) {
      case 'notFound':
        notFound();
      case 'redirect':
        redirect('/login');
      case 'DASHBOARD_FETCH_FAILED':
      case 'UNKNOWN_ERROR':
        return <ErrorAnalyticsDashboard error={result.error} />;
    }
  }

  return <DashboardTemplate viewData={result.value} />;
}
```

## Architecture Flow

```
HTTP Response (HTTP status codes)
    ↓
API Client (catches HTTP errors, throws HttpError)
    ↓
Service (catches HttpError, returns Result<DomainData, DomainError>)
    ↓
PageQuery (uses Service, returns Result<ViewData, PresentationError>)
    ↓
RSC Page (handles PresentationError, renders template or error)
```

## Key Principles

1. **PageQueries use Services**: Never call API Clients directly
2. **Services return Result**: All service methods return `Result<T, DomainError>`
3. **Services handle HTTP errors**: Convert HttpError → DomainError
4. **PageQueries handle presentation errors**: Convert DomainError → PresentationError
5. **No HTTP knowledge in PageQueries**: They don't know about status codes

## Benefits

1. **Clean Architecture**: Each layer has clear boundaries
2. **Type Safety**: Errors are typed at each boundary
3. **Testability**: Can mock services in PageQueries
4. **Flexibility**: Can swap HTTP implementations
5. **Framework Agnostic**: PageQueries remain pure

## Complete Architecture: Read + Write Flows

### Read Flow (PageQueries)

```
HTTP Response (HTTP status codes)
    ↓
API Client (catches HTTP errors, throws HttpError)
    ↓
Service (catches HttpError, returns Result<DomainData, DomainError>)
    ↓
PageQuery (uses Service, returns Result<ViewData, PresentationError>)
    ↓
RSC Page (handles PresentationError, renders template or error)
```

### Write Flow (Mutations)

```
User Action (form submission, button click)
    ↓
Server Action (validates input, creates Command Model)
    ↓
Mutation (uses Service, returns Result<void, MutationError>)
    ↓
Service (catches HttpError, returns Result<void, DomainError>)
    ↓
API Client (throws HttpError on failure)
    ↓
Server Action (handles Result, revalidates data, returns to client)
    ↓
Client Component (shows success message or error)
```

## Mutation Error Handling

### Layer 1: API Client (HTTP → Technical Errors)

```typescript
// apps/website/lib/api/user/UserApiClient.ts
export class UserApiClient {
  async updateUserStatus(userId: string, status: string): Promise<void> {
    const response = await fetch(`${this.baseUrl}/users/${userId}/status`, {
      method: 'PUT',
      body: JSON.stringify({ status }),
      headers: this.getHeaders(),
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new HttpNotFoundError('User not found');
      }
      if (response.status === 403) {
        throw new HttpForbiddenError('Insufficient permissions');
      }
      if (response.status === 400) {
        const error = await response.json();
        throw new HttpValidationError(error.message);
      }
      throw new HttpError(`HTTP ${response.status}`);
    }
  }
}
```

### Layer 2: Service (Technical → Domain Errors)

```typescript
// apps/website/lib/services/user/UserService.ts
import { UserApiClient } from '@/lib/api/user/UserApiClient';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';

export type UserServiceError =
  | { type: 'notFound'; message: string }
  | { type: 'forbidden'; message: string }
  | { type: 'validation'; message: string }
  | { type: 'serverError'; message: string };

export class UserService {
  private apiClient: UserApiClient;

  constructor() {
    // Service creates its own dependencies
    const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
    this.apiClient = new UserApiClient(
      baseUrl,
      new ConsoleErrorReporter(),
      new ConsoleLogger()
    );
  }

  async updateUserStatus(userId: string, status: string): Promise<Result<void, UserServiceError>> {
    try {
      await this.apiClient.updateUserStatus(userId, status);
      return Result.ok(undefined);
    } catch (error) {
      if (error instanceof HttpNotFoundError) {
        return Result.err({ type: 'notFound', message: error.message });
      }
      if (error instanceof HttpForbiddenError) {
        return Result.err({ type: 'forbidden', message: error.message });
      }
      if (error instanceof HttpValidationError) {
        return Result.err({ type: 'validation', message: error.message });
      }
      if (error instanceof HttpError) {
        return Result.err({ type: 'serverError', message: error.message });
      }
      return Result.err({ type: 'serverError', message: 'Unknown error' });
    }
  }
}
```

**Key Points:**
- ✅ Service creates its own API Client
- ✅ Service creates its own Logger and ErrorReporter
- ✅ Catches HTTP errors and converts to domain errors
- ✅ Returns Result type

### Layer 3: Mutation (Domain → Presentation Errors)

```typescript
// apps/website/lib/mutations/UpdateUserStatusMutation.ts
import { UserService, type UserServiceError } from '@/lib/services/user/UserService';

// Presentation error IDs
type UpdateUserStatusError = 'userNotFound' | 'noPermission' | 'invalidData' | 'updateFailed';

export class UpdateUserStatusMutation implements Mutation<UpdateUserStatusInput, void> {
  constructor(private userService: UserService) {}

  async execute(input: UpdateUserStatusInput): Promise<Result<void, UpdateUserStatusError>> {
    const serviceResult = await this.userService.updateUserStatus(input.userId, input.status);
    
    if (serviceResult.isErr()) {
      const error = serviceResult.error;
      
      switch (error.type) {
        case 'notFound':
          return Result.err('userNotFound');
        case 'forbidden':
          return Result.err('noPermission');
        case 'validation':
          return Result.err('invalidData');
        case 'serverError':
          return Result.err('updateFailed');
        default:
          return Result.err('updateFailed');
      }
    }
    
    return Result.ok(undefined);
  }
}
```

### Layer 4: Server Action (Presentation → User)

```typescript
// apps/website/app/actions/userActions.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, Service creates dependencies
  const mutation = new UpdateUserStatusMutation();

  const result = await mutation.execute(input);

  if (result.isErr()) {
    // Return error to client
    return { success: false, error: result.error };
  }

  // Success - revalidate and return
  revalidatePath('/admin/users');
  return { success: true };
}
```

**Key Points:**
- ✅ Server Action constructs only the Mutation
- ✅ Mutation constructs the Service
- ✅ Service constructs its own dependencies
- ✅ No manual wiring needed

### Layer 5: Client Component (Handles Result)

```typescript
// apps/website/components/admin/UserStatusForm.tsx
'use client';

import { updateUserStatus } from '@/app/actions/userActions';
import { useState } from 'react';

export function UserStatusForm({ userId }: { userId: string }) {
  const [status, setStatus] = useState('active');
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError(null);
    setSuccess(false);

    const result = await updateUserStatus({ userId, status });

    if (result.success) {
      setSuccess(true);
    } else {
      // Map mutation error to user-friendly message
      switch (result.error) {
        case 'userNotFound':
          setError('User not found');
          break;
        case 'noPermission':
          setError('You do not have permission to update this user');
          break;
        case 'invalidData':
          setError('Invalid status selected');
          break;
        case 'updateFailed':
          setError('Failed to update user status');
          break;
      }
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <select value={status} onChange={(e) => setStatus(e.target.value)}>
        <option value="active">Active</option>
        <option value="suspended">Suspended</option>
      </select>
      <button type="submit">Update</button>
      {error && <div className="error">{error}</div>}
      {success && <div className="success">User updated!</div>}
    </form>
  );
}
```

## Complete Error Flow Comparison

### Read Flow (PageQuery)
```
HTTP 404 → HttpNotFoundError → Service Error {type: 'notFound'} → PageQuery Error 'notFound' → notFound()
```

### Write Flow (Mutation)
```
HTTP 404 → HttpNotFoundError → Service Error {type: 'notFound'} → Mutation Error 'userNotFound' → Client shows "User not found"
```

## Key Differences

| Aspect | PageQuery | Mutation |
|--------|-----------|----------|
| **Purpose** | Fetch data for rendering | Perform write operations |
| **Return Type** | `Result<ViewData, PresentationError>` | `Result<void, MutationError>` |
| **Error Handling** | RSC handles (notFound, redirect) | Client handles (show message) |
| **Side Effects** | None (pure) | Revalidation, cache invalidation |
| **User Feedback** | Error page or redirect | Toast message or inline error |

## Implementation Steps

1. **Update Services**: Return `Result<T, DomainError>` for both read and write
2. **Update PageQueries**: Use Services, map DomainError → PresentationError
3. **Update Mutations**: Use Services, map DomainError → MutationError
4. **Remove ErrorWithStatusCode**: Delete from all PageQueries
5. **Add ESLint Rules**: Prevent status codes and direct API usage

## ESLint Rules Needed

1. **No status codes in PageQueries**: Prevent `statusCode` checks
2. **No status codes in Mutations**: Prevent `statusCode` checks
3. **PageQueries must use Services**: Prevent direct API client usage
4. **Mutations must use Services**: Prevent direct API client usage
5. **Services must return Result**: Enforce Result pattern

This complete architecture handles both read and write flows with clean separation of concerns.
