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
- MUST be deterministic.
- MUST be side-effect free.
- MUST NOT perform HTTP or call API clients.
- Input: API DTO.
- Output: ViewData (Plain JSON).
- MUST live in
lib/builders/view-data/**. - MUST use
static build()andsatisfies ViewDataBuilder. - 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
LeagueViewDataBuilderRaceViewDataBuilder
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-contractgridpilot-rules/filename-view-data-builder-matchgridpilot-rules/formatters-must-return-primitives
See docs/architecture/website/WEBSITE_CONTRACT.md for the authoritative contract.