Files
gridpilot.gg/docs/architecture/DATA_FLOW.md
2025-12-17 23:14:07 +01:00

8.8 KiB

Frontend data shapes: View Models, Presenters, and API Client (Strict)

This document defines the exact placement and responsibilities for frontend data shapes. It is designed to leave no room for interpretation.

  1. Definitions

API DTOs

Transport shapes owned by the API boundary (HTTP). They are not used directly by UI components.

View Models

UI-owned shapes. They represent exactly what the UI needs and nothing else.

Website Presenters

Pure mappers that convert API DTOs (or core use-case outputs) into View Models.

API Client

A thin HTTP wrapper that returns API DTOs only and performs no business logic.

  1. Directory layout (exact)

apps/website ├── app/ # Next.js routes/pages ├── components/ # React components (UI only) ├── lib/ │ ├── api/ # API client (HTTP only) │ ├── dtos/ # API DTO types (transport shapes) │ ├── view-models/ # View Models (UI-owned shapes) │ ├── presenters/ # Presenters: DTO -> ViewModel mapping │ ├── services/ # UI orchestration (calls api + presenters) │ └── index.ts

No additional folders for these concerns are allowed.

  1. View Models (placement and rules)

Where they live

View Models MUST live in:

apps/website/lib/view-models

What they may contain • UI-ready primitives (strings, numbers, booleans) • UI-specific derived fields (e.g., isOwner, badgeLabel, formattedDate) • UI-specific structures (e.g., grouped arrays, flattened objects)

What they must NOT contain • Domain entities or value objects • API transport metadata • Validation logic • Network or persistence concerns

Rule

Components consume only View Models.

  1. API DTOs in the website (placement and rules)

Clarification

The website does have DTOs, but only API DTOs.

These DTOs exist exclusively to type HTTP communication with the backend API. They are not UI models.

Where they live

Website-side API DTO types MUST live in:

apps/website/lib/dtos

What they represent • Exact transport shapes sent/received via HTTP • Backend API contracts • No UI assumptions

Who may use them • API client • Website presenters

Who must NOT use them • React components • Pages • UI logic

Rule

API DTOs stop at the presenter boundary.

Components must never consume API DTOs directly.

  1. Presenters (website) (placement and rules)

Where they live

Website presenters MUST live in:

apps/website/lib/presenters

What they do • Convert API DTOs into View Models • Perform UI-friendly formatting and structuring • Are pure and deterministic

What they must NOT do • Make API calls • Read from localStorage/cookies directly • Contain business rules or decisions • Perform side effects

Rule

Presenters output View Models. Presenters never output API DTOs.

  1. Do website presenters use View Models?

Yes. Strictly:

Website presenters MUST output View Models and MUST NOT output API DTOs.

Flow is always:

API DTO -> Presenter -> View Model -> Component

  1. API client (website) (placement and rules)

Where it lives

The API client MUST live in:

apps/website/lib/api

What it does • Sends HTTP requests • Returns API DTOs • Performs authentication header/cookie handling only at transport level • Does not map to View Models

What it must NOT do • Format or reshape responses for UI • Contain business rules • Contain decision logic

Rule

The API client has no knowledge of View Models.

  1. Website service layer (strict orchestration)

Where it lives

Website orchestration MUST live in:

apps/website/lib/services

What it does • Calls the API client • Calls presenters to map DTO -> View Model • Returns View Models to pages/components

What it must NOT do • Contain domain logic • Modify core invariants • Return API DTOs

Rule

Services are the only layer allowed to call both api/ and presenters/.

Components must not call the API client directly.

  1. Allowed dependency directions (frontend)

Within apps/website:

components -> services -> (api + presenters) -> (dtos + view-models)

Strict rules: • components may import only view-models and services • presenters may import dtos and view-models only • api may import dtos only • services may import api, presenters, view-models

Forbidden: • components importing api • components importing dtos • presenters importing api • api importing view-models • any website code importing core domain entities

  1. Naming rules (strict) • View Models end with ViewModel • API DTOs end with Dto • Presenters end with Presenter • Services end with Service • One export per file • File name equals exported symbol (PascalCase)

  1. Final "no ambiguity" summary • View Models live in apps/website/lib/view-models • API DTOs live in apps/website/lib/dtos • Presenters live in apps/website/lib/presenters and map DTO -> ViewModel • API client lives in apps/website/lib/api and returns DTOs only • Services live in apps/website/lib/services and return View Models only • Components consume View Models only and never touch API DTOs or API clients

  1. Clean Architecture Flow Diagram
graph TD
    A[UI Components] --> B[Services]
    B --> C[API Client]
    B --> D[Presenters]
    C --> E[API DTOs]
    D --> E
    D --> F[View Models]
    A --> F

    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#fff3e0
    style D fill:#e8f5e8
    style E fill:#ffebee
    style F fill:#e3f2fd

Flow Explanation:

  • UI Components consume only View Models
  • Services orchestrate API calls and presenter mappings
  • API Client returns raw API DTOs
  • Presenters transform API DTOs into UI-ready View Models
  • Strict dependency direction: UI → Services → (API + Presenters) → (DTOs + ViewModels)

  1. Enforcement Guidelines

ESLint Rules:

  • Direct imports from apiClient are forbidden - use services instead
  • Direct imports from dtos in UI components are forbidden - use ViewModels instead
  • Direct imports from api/* in UI components are forbidden - use services instead

TypeScript Path Mappings:

  • Use @/lib/dtos for API DTO imports
  • Use @/lib/view-models for View Model imports
  • Use @/lib/presenters for Presenter imports
  • Use @/lib/services for Service imports
  • Use @/lib/api for API client imports

Import Restrictions:

  • Components may import only view-models and services
  • Presenters may import dtos and view-models only
  • API may import dtos only
  • Services may import api, presenters, view-models
  • Forbidden: components importing api, components importing dtos, presenters importing api, api importing view-models

Verification Commands:

npm run build      # Ensure TypeScript compiles
npm run lint       # Ensure ESLint rules pass
npm run test       # Ensure all tests pass

  1. Architecture Examples

Before (Violates Rules):

// In a page component - BAD
import { apiClient } from '@/lib/apiClient';
import type { RaceResultDto } from '@/lib/dtos/RaceResultDto';

const RacePage = () => {
  const [data, setData] = useState<RaceResultDto[]>();
  // Direct API call and DTO usage in UI
  useEffect(() => {
    apiClient.getRaceResults().then(setData);
  }, []);
  return <div>{data?.map(d => d.position)}</div>;
};

After (Clean Architecture):

// In a page component - GOOD
import { RaceResultsService } from '@/lib/services/RaceResultsService';
import type { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel';

const RacePage = () => {
  const [data, setData] = useState<RaceResultViewModel[]>();
  useEffect(() => {
    RaceResultsService.getResults().then(setData);
  }, []);
  return <div>{data?.map(d => d.formattedPosition)}</div>;
};

Service Implementation:

// apps/website/lib/services/RaceResultsService.ts
import { apiClient } from '@/lib/api';
import { RaceResultsPresenter } from '@/lib/presenters/RaceResultsPresenter';

export class RaceResultsService {
  static async getResults(): Promise<RaceResultViewModel[]> {
    const dtos = await apiClient.getRaceResults();
    return RaceResultsPresenter.present(dtos);
  }
}

Presenter Implementation:

// apps/website/lib/presenters/RaceResultsPresenter.ts
import type { RaceResultDto } from '@/lib/dtos/RaceResultDto';
import type { RaceResultViewModel } from '@/lib/view-models/RaceResultViewModel';

export class RaceResultsPresenter {
  static present(dtos: RaceResultDto[]): RaceResultViewModel[] {
    return dtos.map(dto => ({
      id: dto.id,
      formattedPosition: `${dto.position}${dto.position === 1 ? 'st' : dto.position === 2 ? 'nd' : dto.position === 3 ? 'rd' : 'th'}`,
      driverName: dto.driverName,
      // ... other UI-specific formatting
    }));
  }
}