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

4.8 KiB

Builders (Strict)

This document defines the Builder pattern for apps/website.

Builders exist to transform data between presentation model types.

1) Definition

A Builder is a deterministic, side-effect free transformation between website presentation models.

There are two types of builders:

1.1 ViewModel Builders

Transform API Transport DTOs into ViewModels.

Purpose: Prepare raw API data for client-side state management.

Location: apps/website/lib/builders/view-models/**

Pattern:

export class AdminViewModelBuilder {
  static build(dto: UserDto): AdminUserViewModel {
    return new AdminUserViewModel(dto);
  }
}

1.2 ViewData Builders

Transform API DTOs directly into ViewData for templates.

Purpose: Prepare API data for server-side rendering without ViewModels.

Location: apps/website/lib/builders/view-data/**

Pattern:

export class LeagueViewDataBuilder {
  static build(apiDto: LeagueApiDto): LeagueDetailViewData {
    return {
      leagueId: apiDto.id,
      name: apiDto.name,
      // ... more fields
    };
  }
}

2) Non-negotiable rules

ViewModel Builders

  1. MUST be deterministic
  2. MUST be side-effect free
  3. MUST NOT perform HTTP
  4. MUST NOT call API clients
  5. MUST NOT access cookies/headers
  6. Input: API Transport DTO
  7. Output: ViewModel
  8. MUST live in lib/builders/view-models/**

ViewData Builders

  1. MUST be deterministic
  2. MUST be side-effect free
  3. MUST NOT perform HTTP
  4. MUST NOT call API clients
  5. MUST NOT access cookies/headers
  6. Input: API DTO
  7. Output: ViewData
  8. MUST live in lib/builders/view-data/**

3) Why two builder types?

ViewModel Builders (API → Client State):

  • Bridge the API boundary
  • Convert transport types to client classes
  • Add client-only fields if needed
  • Run in client code

ViewData Builders (API → Render Data):

  • Bridge the presentation boundary
  • Transform API data directly for templates
  • Format values for display
  • Run in server code (RSC)

4) Relationship to other patterns

API Transport DTO
    ↓
ViewModel Builder (lib/builders/view-models/)
    ↓
ViewModel (lib/view-models/)
    ↓
    (for client components)

API Transport DTO
    ↓
ViewData Builder (lib/builders/view-data/)
    ↓
ViewData (lib/templates/)
    ↓
Template (lib/templates/)

5) Naming convention

ViewModel Builders: *ViewModelBuilder

  • AdminViewModelBuilder
  • RaceViewModelBuilder

ViewData Builders: *ViewDataBuilder

  • LeagueViewDataBuilder
  • RaceViewDataBuilder

6) File structure

lib/
  builders/
    view-models/
      AdminViewModelBuilder.ts
      RaceViewModelBuilder.ts
      index.ts
      
    view-data/
      LeagueViewDataBuilder.ts
      RaceViewDataBuilder.ts
      index.ts

7) Usage examples

ViewModel Builder (Client Component)

'use client';

import { AdminViewModelBuilder } from '@/lib/builders/view-models/AdminViewModelBuilder';
import { AdminApiClient } from '@/lib/api/admin/AdminApiClient';

export function AdminPage() {
  const [users, setUsers] = useState<AdminUserViewModel[]>([]);
  
  useEffect(() => {
    const apiClient = new AdminApiClient();
    const dto = await apiClient.getUsers();
    const viewModels = dto.map(d => AdminViewModelBuilder.build(d));
    setUsers(viewModels);
  }, []);
  
  // ... render with viewModels
}

ViewData Builder (Server Component)

import { LeagueViewDataBuilder } from '@/lib/builders/view-data/LeagueViewDataBuilder';
import { LeagueDetailPageQuery } from '@/lib/page-queries/LeagueDetailPageQuery';

export default async function LeagueDetailPage({ params }) {
  const apiDto = await LeagueDetailPageQuery.execute(params.id);
  const viewData = LeagueViewDataBuilder.build(apiDto);
  
  return <LeagueDetailTemplate viewData={viewData} />;
}

8) Common mistakes

Wrong: Using "Presenter" for DTO → ViewModel

// DON'T
export class AdminPresenter {
  static createViewModel(dto: UserDto): AdminUserViewModel { ... }
}

Correct: Use ViewModelBuilder

export class AdminViewModelBuilder {
  static build(dto: UserDto): AdminUserViewModel { ... }
}

Wrong: Using "Transformer" for ViewModel → ViewData

// DON'T
export class RaceResultsDataTransformer {
  static transform(...): TransformedData { ... }
}

Correct: Use ViewDataBuilder

export class RaceResultsViewDataBuilder {
  static build(...): RaceResultsViewData { ... }
}

9) Enforcement

These rules are enforced by ESLint:

  • gridpilot-rules/view-model-builder-contract
  • gridpilot-rules/view-data-builder-contract
  • gridpilot-rules/filename-view-model-builder-match
  • gridpilot-rules/filename-view-data-builder-match

See docs/architecture/website/WEBSITE_GUARDRAILS.md for details.