Files
gridpilot.gg/docs/architecture/website/BUILDERS.md
2026-01-24 01:22:43 +01:00

3.4 KiB

Builders (Strict)

This document defines the Builder pattern for apps/website.

Builders exist to transform raw API data into flat, serializable ViewData.

1) Definition

A Builder is a deterministic, side-effect free transformation that bridges the boundary between the API (DTOs) and the Template (ViewData).

1.1 ViewData Builders

Transform API DTOs directly into ViewData for templates.

Purpose: Prepare API data for server-side rendering. They ensure that logic-rich behavior is stripped away, leaving only a "dumb" JSON structure safe for SSR and hydration.

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

Pattern:

import { ViewDataBuilder } from '@/lib/contracts/builders/ViewDataBuilder';

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

// Enforce static compliance without dummy instances
LeagueViewDataBuilder satisfies ViewDataBuilder<LeagueApiDto, LeagueDetailViewData>;

2) Non-negotiable rules

ViewData Builders

  1. MUST be deterministic.
  2. MUST be side-effect free.
  3. MUST NOT perform HTTP or call API clients.
  4. Input: API DTO.
  5. Output: ViewData (Plain JSON).
  6. MUST live in lib/builders/view-data/**.
  7. MUST use static build() and satisfies ViewDataBuilder.
  8. MUST use Formatters for primitive output (strings/numbers).

3) Why no ViewModel Builders?

ViewModels are self-building.

A ViewModel is a class that wraps data to provide behavior. Instead of a separate builder class, ViewModels are instantiated directly from ViewData using their Constructor. This removes unnecessary "ceremony" and keeps the API unambiguous.

Redundant Pattern (Forbidden):

// Why have this extra class?
export class TeamViewModelBuilder {
  static build(data: TeamViewData): TeamViewModel {
    return new TeamViewModel(data);
  }
}

Clean Pattern (Required):

// Just use the class itself in the ClientWrapper
const vm = new TeamViewModel(viewData);

4) Relationship to other patterns

API Transport DTO
    ↓
ViewData Builder (lib/builders/view-data/)
    ↓
Formatters (lib/display-objects/) -- [primitive output]
    ↓
ViewData (Plain JSON)
    ↓
Template (SSR)
    ↓
ViewModel (lib/view-models/) -- [new ViewModel(viewData)]
    ↓
Display Objects (lib/display-objects/) -- [rich API]

5) Naming convention

ViewData Builders: *ViewDataBuilder

  • LeagueViewDataBuilder
  • RaceViewDataBuilder

6) Usage example (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);
  
  // Transform to flat JSON for SSR
  const viewData = LeagueViewDataBuilder.build(apiDto);
  
  return <LeagueDetailTemplate viewData={viewData} />;
}

7) Enforcement

These rules are enforced by ESLint:

  • gridpilot-rules/view-data-builder-contract
  • gridpilot-rules/filename-view-data-builder-match
  • gridpilot-rules/formatters-must-return-primitives

See docs/architecture/website/WEBSITE_CONTRACT.md for the authoritative contract.