This commit is contained in:
2026-01-11 14:04:48 +01:00
parent 971aa7288b
commit 2f0b83f030
10 changed files with 436 additions and 173 deletions

View File

@@ -1,168 +0,0 @@
# Contract Testing Quick Start Guide
## 🚀 Quick Setup
### 1. Run the Full Contract Test Suite
```bash
npm run test:contracts
```
This single command will:
- ✅ Validate API contracts
- ✅ Generate OpenAPI spec
- ✅ Generate TypeScript types
- ✅ Check for breaking changes
- ✅ Verify website type compatibility
### 2. Individual Commands
```bash
# Validate API contracts only
npm run test:api:contracts
# Generate types (after making DTO changes)
npm run api:sync-types
# Check compatibility (detect breaking changes)
npm run test:contract:compatibility
# Verify website can consume types
npm run website:type-check
```
## 📁 What Gets Created
### Generated Types
- **Location**: `apps/website/lib/types/generated/`
- **Files**: One `.ts` file per DTO (e.g., `RaceDTO.ts`, `DriverDTO.ts`)
- **Usage**: Import directly in website code
### Test Files
- **API Tests**: `apps/api/src/shared/testing/contractValidation.test.ts`
- **Website Tests**: `apps/website/lib/types/contractConsumption.test.ts`
### CI/CD
- **Workflow**: `.github/workflows/contract-testing.yml`
- **Triggers**: Pull requests and main branch pushes
## 🎯 Common Workflows
### Making API Changes
1. **Update DTO in API**:
```typescript
// apps/api/src/domain/race/dtos/RaceDTO.ts
export class RaceDTO {
@ApiProperty()
id: string;
@ApiProperty()
name: string;
@ApiProperty({ required: false })
description?: string; // New optional field
}
```
2. **Run contract tests**:
```bash
npm run test:contracts
```
3. **If tests pass**, commit your changes. The CI will regenerate types automatically.
### Updating Website Code
1. **Import generated types**:
```typescript
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
function RaceComponent({ race }: { race: RaceDTO }) {
return <div>{race.name}</div>;
}
```
2. **TypeScript will catch errors** if contracts change.
### Detecting Breaking Changes
```bash
# This will show you exactly what changed
npm run test:contract:compatibility
```
Output example:
```
🚨 BREAKING CHANGES DETECTED:
• RaceDTO.status: Property status was removed (BREAKING)
❌ REMOVED:
• OldDTO: DTO OldDTO was removed
ADDED:
• NewDTO: New DTO NewDTO was added
```
## 🔧 Troubleshooting
### "Cannot find module" errors
```bash
# Regenerate types
npm run api:sync-types
```
### Type generation fails
1. Check DTOs have `@ApiProperty` decorators
2. Verify OpenAPI spec is valid: `cat apps/api/openapi.json`
3. Run individual steps:
```bash
npm run api:generate-spec
npm run api:generate-types
```
### CI fails on breaking changes
- Review what changed
- Update website code to handle new types
- Or revert the breaking change if unintended
## 📋 Checklist Before Committing
- [ ] Run `npm run test:contracts` locally
- [ ] All tests pass
- [ ] No breaking changes (or they're intentional)
- [ ] Website code updated to handle new types
- [ ] Generated types are committed (if needed)
## 🎓 Key Concepts
### What is a "Contract"?
A contract is the agreement between API and website about what data looks like:
- DTO definitions
- Property types
- Required vs optional fields
### What are "Breaking Changes"?
Changes that would break the website:
- Removing required fields
- Changing field types
- Adding required fields to existing DTOs
### Why Generate Types?
- **Type Safety**: Catch errors at compile time
- **Auto-completion**: Better IDE experience
- **Documentation**: Types serve as living documentation
- **Consistency**: Single source of truth
## 🚨 Important Notes
1. **Never manually edit generated files** - they're auto-generated
2. **Always run tests before committing** - prevents breaking changes
3. **The CI will regenerate types** - but local verification is faster
4. **Breaking changes need review** - consider versioning strategy
## 📚 More Resources
- Full documentation: `docs/CONTRACT_TESTING.md`
- API examples: `apps/api/src/shared/testing/contractValidation.test.ts`
- Website examples: `apps/website/lib/types/contractConsumption.test.ts`
- CI/CD workflow: `.github/workflows/contract-testing.yml`

View File

@@ -0,0 +1,60 @@
# Client State (Strict)
This document defines the only allowed usage of client state in `apps/website`.
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
## 1) Core rule
Client state is allowed only for **UI concerns**.
The API remains the single source of truth.
## 2) Allowed client state
Client state MAY represent:
- selection (table rows, active tab)
- open/closed dialogs and drawers
- input values before submission
- loading flags
- best-effort optimistic flags
## 3) Forbidden client state
Client state MUST NOT be used as:
- business truth
- security truth
- permission truth
Examples of forbidden behavior:
- client code deciding that a user is allowed based on local flags
- client code persisting an authoritative list state that overrides server truth
## 4) Conflict resolution rule (hard)
If client state and API truth disagree, **API truth wins**.
Correct handling is:
- show the API result
- revalidate and reload server-rendered truth
## 5) Relationship to Blockers
Blockers exist to prevent UX mistakes.
- Blockers are not security.
- Blockers may reduce unnecessary requests.
- The API still enforces rules.
See [`BLOCKER_GUARDS.md`](docs/architecture/website/BLOCKER_GUARDS.md:1).
## 6) Canonical placement in this repo
- `apps/website/lib/blockers/**`
- `apps/website/lib/hooks/**`
- `apps/website/lib/command-models/**`

View File

@@ -25,7 +25,7 @@ They exist to avoid duplicating presentation logic across View Models.
A Display Object MAY: A Display Object MAY:
- format values (money, dates, durations) - format values (money, dates, durations)
- handle localization and language-specific rules - handle localization only when localization inputs are deterministic (for example: mapping stable codes to stable labels)
- map codes to labels - map codes to labels
- encapsulate UI display conventions - encapsulate UI display conventions
- be reused across multiple View Models - be reused across multiple View Models
@@ -67,6 +67,20 @@ In this repository, a Display Object MUST NOT:
Reason: these are runtime-locale/timezone dependent and cause SSR/hydration mismatches. Reason: these are runtime-locale/timezone dependent and cause SSR/hydration mismatches.
## Localization rule (strict)
Localization MUST NOT depend on runtime locale APIs.
Allowed approaches:
- API returns the exact labels/strings for the current user context.
- Website maps stable codes to stable labels using a deterministic table.
Forbidden approaches:
- any usage of `Intl.*`
- any usage of `toLocale*`
If a rule affects system correctness or persistence, If a rule affects system correctness or persistence,
it does not belong in a Display Object. it does not belong in a Display Object.

View File

@@ -5,6 +5,8 @@ It applies to all write operations (create, update, delete).
There are no exceptions. There are no exceptions.
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
Core Principle Core Principle
@@ -13,6 +15,21 @@ Read and Write paths are different.
What is displayed is never sent back. What is displayed is never sent back.
## Non-negotiable write boundary
All writes MUST enter the system through **Next.js Server Actions**.
Forbidden:
- client components performing write HTTP requests
- client components calling API clients for mutations
Allowed:
- client submits intent (FormData, button action)
- server action performs UX validation
- server action calls the API
High-Level Flow High-Level Flow

View File

@@ -0,0 +1,56 @@
# Presenters (Strict)
This document defines the **Presenter** boundary for `apps/website`.
Presenters exist to prevent responsibility drift into:
- server routes
- Page Queries
- Templates
## 1) Definition
A **Presenter** is a deterministic, side-effect free transformation between website presentation models.
Allowed transformations:
- Page DTO → ViewData
- Page DTO → ViewModel
- ViewModel → ViewData
## 2) Non-negotiable rules
1. Presenters MUST be deterministic.
2. Presenters MUST be side-effect free.
3. Presenters MUST NOT perform HTTP.
4. Presenters MUST NOT call API clients.
5. Presenters MUST NOT access cookies/headers.
6. Presenters MAY use Display Objects.
7. Presenters MUST NOT import Templates.
## 3) Where Presenters run
Presenters run in **client code only**.
Presenters MUST be defined in `'use client'` modules.
If a computation affects routing decisions (redirect, notFound), it belongs in a Page Query or server route composition, not in a Presenter.
## 4) Relationship to Display Objects
Display Objects implement reusable formatting/mapping.
Rules:
- Presenters may orchestrate Display Objects.
- Display Object instances MUST NOT appear in ViewData.
See [`DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:1) and [`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:1).
## 5) Canonical placement in this repo (strict)
Presenters MUST live colocated with ViewModels under:
- `apps/website/lib/view-models/**`
Reason: this repo already treats `apps/website/lib/view-models/**` as the client-only presentation module boundary.

View File

@@ -2,6 +2,8 @@
ViewData is the **only** allowed input type for Templates in `apps/website`. ViewData is the **only** allowed input type for Templates in `apps/website`.
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
## 1) Definition ## 1) Definition
ViewData is a JSON-serializable, template-ready data structure: ViewData is a JSON-serializable, template-ready data structure:
@@ -14,7 +16,7 @@ ViewData is a JSON-serializable, template-ready data structure:
ViewData is not: ViewData is not:
- a Page DTO (raw transport) - an API Transport DTO (raw transport)
- a ViewModel (client-only class) - a ViewModel (client-only class)
- a Display Object instance - a Display Object instance
@@ -25,8 +27,13 @@ ViewData MUST be created in client code:
1) Initial SSR-safe render: `ViewData = fromDTO(PageDTO)` 1) Initial SSR-safe render: `ViewData = fromDTO(PageDTO)`
2) Post-hydration render: `ViewData = fromViewModel(ViewModel)` 2) Post-hydration render: `ViewData = fromViewModel(ViewModel)`
These transformations are Presenters.
See [`PRESENTERS.md`](docs/architecture/website/PRESENTERS.md:1).
Templates MUST NOT compute derived values. Templates MUST NOT compute derived values.
Presenters MUST NOT call the API.
## 4) Determinism rules ## 4) Determinism rules
Any formatting used to produce ViewData MUST be deterministic. Any formatting used to produce ViewData MUST be deterministic.
@@ -38,9 +45,11 @@ Forbidden anywhere in formatting code paths:
Reason: SSR and browser outputs can differ. Reason: SSR and browser outputs can differ.
Localization MUST NOT depend on runtime locale APIs.
If localized strings are required, they MUST be provided as deterministic inputs (for example via API-provided labels or a deterministic code-to-label map) and passed through Presenters into ViewData.
## 5) Relationship to Display Objects ## 5) Relationship to Display Objects
Display Objects are used to implement formatting/mapping, but their instances MUST NOT be stored inside ViewData. Display Objects are used to implement formatting/mapping, but their instances MUST NOT be stored inside ViewData.
Only primitive outputs produced by Display Objects may appear in ViewData. Only primitive outputs produced by Display Objects may appear in ViewData.

View File

@@ -0,0 +1,253 @@
# Website Architecture Contract (Strict)
This document is the **authoritative contract** for `apps/website`.
If any other website document conflicts with this one, **this one wins**.
## 1) Purpose (non-negotiable)
The website is a **delivery layer**.
It does **not**:
- contain business rules
- make authorization decisions
- own or persist business truth
It **only**:
- renders truth from `apps/api`
- collects user intent
- forwards user intent to `apps/api`
The API is the single source of truth.
## 2) System context (hard boundary)
The website never bypasses the API.
```text
Browser
Next.js App Router (RSC + Server Actions)
HTTP
Backend API (Use Cases, Domain, Database)
```
## 3) Website presentation model types (strict)
### 3.1 API Transport DTO
Definition: the shape returned by the backend API over HTTP.
Rules:
- API Transport DTOs MUST be contained inside infrastructure.
- API Transport DTOs MUST NOT be imported by Templates.
Canonical placement in this repo:
- `apps/website/lib/types/**` (transport DTOs consumed by services and page queries)
### 3.2 Page DTO
Definition: the website-owned, server-to-client payload for a route.
Rules:
- JSON-serializable only.
- Contains **raw** values only (IDs, ISO strings, numbers, codes).
- MUST NOT contain class instances.
- Created by Page Queries.
- Passed from server routes into client code.
Canonical placement in this repo:
- `apps/website/lib/page-queries/**` (composition and Page DTO construction)
### 3.3 ViewModel
Definition: the client-only, UI-owned class representing fully prepared UI state.
Rules:
- Instantiated only in `'use client'` modules.
- Never serialized.
- MUST NOT be passed into Templates.
See [`VIEW_MODELS.md`](docs/architecture/website/VIEW_MODELS.md:1).
Canonical placement in this repo:
- `apps/website/lib/view-models/**`
### 3.4 ViewData
Definition: the only allowed input type for Templates.
Rules:
- JSON-serializable only.
- Contains only template-ready values (mostly strings/numbers/booleans).
- MUST NOT contain class instances.
See [`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:1).
Canonical placement in this repo:
- `apps/website/templates/**` (Templates that accept ViewData only)
## 4) Presentation helpers (strict)
### 4.1 Presenter
Definition: a deterministic, side-effect free transformation.
Presenters map between website presentation models:
- Page DTO → ViewData
- Page DTO → ViewModel
- ViewModel → ViewData
Rules:
- MUST be deterministic.
- MUST be side-effect free.
- MUST NOT call HTTP.
- MUST NOT call the API.
- MAY use Display Objects.
See [`PRESENTERS.md`](docs/architecture/website/PRESENTERS.md:1).
Canonical placement in this repo:
- colocated with ViewModels in `apps/website/lib/view-models/**`
### 4.2 Display Object
Definition: deterministic, reusable, UI-only formatting/mapping logic.
Rules:
- Class-based, immutable, deterministic.
- MUST NOT call `Intl.*`.
- MUST NOT call any `toLocale*`.
- MUST NOT implement business rules.
See [`DISPLAY_OBJECTS.md`](docs/architecture/website/DISPLAY_OBJECTS.md:1).
Canonical placement in this repo:
- `apps/website/lib/display-objects/**`
## 5) Read flow (strict)
```text
RSC page.tsx
PageQuery (server)
API service / API client (infra)
API Transport DTO
Page DTO
Presenter (client)
ViewModel (optional, client)
Presenter (client)
ViewData
Template
```
## 6) Write flow (strict)
All writes MUST enter through **Next.js Server Actions**.
Forbidden:
- client components performing write HTTP requests
- client components calling API clients for mutations
Allowed:
- client submits intent (FormData, button action)
- server action performs UX validation
- server action calls the API
See [`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:1).
## 7) Authorization (strict)
- The website may hide/disable UI for UX.
- The website MUST NOT enforce security.
- The API enforces authentication and authorization.
See [`BLOCKER_GUARDS.md`](docs/architecture/website/BLOCKER_GUARDS.md:1).
## 7.1) Client state (strict)
Client-side state is allowed only for UI concerns.
Allowed:
- selection
- open/closed dialogs
- transient form state
- optimistic flags and loading spinners
Forbidden:
- treating client state as business truth
- using client state as an authorization decision
- persisting client state as the source of truth
Hard rule:
- any truth returned by the API MUST overwrite client assumptions.
Canonical placement in this repo:
- `apps/website/lib/blockers/**` for UX-only prevention helpers
- `apps/website/lib/hooks/**` for React-only utilities
- `apps/website/lib/command-models/**` for transient form models
See [`CLIENT_STATE.md`](docs/architecture/website/CLIENT_STATE.md:1).
## 8) DI contract (Inversify) (strict)
The DI system under [`apps/website/lib/di/index.ts`](apps/website/lib/di/index.ts:1) is **client-first**.
Server execution is concurrent. Any shared singleton container can leak cross-request state.
Rules:
1. Server `app/**/page.tsx` MUST NOT access the container.
2. Page Queries SHOULD prefer manual wiring.
3. Client modules MAY use DI via [`ContainerProvider`](apps/website/lib/di/index.ts:11) and hooks.
4. [`ContainerManager.getContainer()`](apps/website/lib/di/container.ts:74) is **client-only**.
5. Any server DI usage MUST be request-scoped (a fresh container per request).
Hard constraint:
- A singleton Inversify container MUST NOT be used to serve concurrent server requests.
See [`WEBSITE_DI_RULES.md`](docs/architecture/website/WEBSITE_DI_RULES.md:1).
## 9) Non-negotiable rules (final)
1. The API is the brain.
2. The website is a terminal.
3. API Transport DTOs never reach Templates.
4. ViewModels never go to the API.
5. Templates accept ViewData only.
6. Page Queries do not format; they only compose.
7. Presenters are pure and deterministic.
8. Server Actions are the only write entry point.
9. Authorization always belongs to the API.

View File

@@ -2,6 +2,8 @@
This repo uses Inversify DI under [apps/website/lib/di](apps/website/lib/di/index.ts:1). This repo uses Inversify DI under [apps/website/lib/di](apps/website/lib/di/index.ts:1).
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
## 1) Non-negotiable safety rule ## 1) Non-negotiable safety rule
No stateful service instances may be shared across requests. No stateful service instances may be shared across requests.
@@ -20,6 +22,8 @@ Reason: Next.js server execution is concurrent; shared state causes cross-reques
- SHOULD prefer explicit construction (manual wiring). - SHOULD prefer explicit construction (manual wiring).
- MAY use DI only if all resolved services are stateless and safe for concurrent requests. - MAY use DI only if all resolved services are stateless and safe for concurrent requests.
If DI is used on the server, it MUST be request-scoped (a new container per request) and MUST NOT reuse a shared singleton container.
### 2.3 Client modules ### 2.3 Client modules
- MAY use DI via `ContainerProvider` and hooks (example: `useInject`). - MAY use DI via `ContainerProvider` and hooks (example: `useInject`).
@@ -28,3 +32,7 @@ Reason: Next.js server execution is concurrent; shared state causes cross-reques
[`ContainerManager`](apps/website/lib/di/container.ts:61) holds a singleton container. Treat it as **unsafe for server request scope** unless proven otherwise. [`ContainerManager`](apps/website/lib/di/container.ts:61) holds a singleton container. Treat it as **unsafe for server request scope** unless proven otherwise.
Strict rule:
- [`ContainerManager.getContainer()`](apps/website/lib/di/container.ts:74) is client-only.

View File

@@ -2,11 +2,14 @@
This document defines architecture guardrails that must be enforced via tests + ESLint. This document defines architecture guardrails that must be enforced via tests + ESLint.
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
## 1) RSC boundary guardrails ## 1) RSC boundary guardrails
Fail CI if any `apps/website/app/**/page.tsx`: Fail CI if any `apps/website/app/**/page.tsx`:
- imports from `apps/website/lib/view-models/*` - imports from `apps/website/lib/view-models/*`
- imports from Presenter code (presenters live colocated with ViewModels)
- calls `Intl.*` or `toLocale*` - calls `Intl.*` or `toLocale*`
- performs sorting/filtering (`sort`, `filter`, `reduce`) beyond trivial null checks - performs sorting/filtering (`sort`, `filter`, `reduce`) beyond trivial null checks
@@ -15,6 +18,7 @@ Fail CI if any `apps/website/app/**/page.tsx`:
Fail CI if any `apps/website/templates/**`: Fail CI if any `apps/website/templates/**`:
- imports from `apps/website/lib/view-models/*` - imports from `apps/website/lib/view-models/*`
- imports from presenter code (presenters live colocated with ViewModels)
- imports from `apps/website/lib/display-objects/*` - imports from `apps/website/lib/display-objects/*`
- calls `Intl.*` or `toLocale*` - calls `Intl.*` or `toLocale*`
@@ -27,4 +31,3 @@ Fail CI if any `apps/website/lib/display-objects/**`:
- calls `Intl.*` or `toLocale*` - calls `Intl.*` or `toLocale*`
Display Objects must be deterministic. Display Objects must be deterministic.

View File

@@ -4,6 +4,8 @@ This document defines the only allowed presentation architecture for `apps/websi
It is **website-only** and does not change `apps/api` or `core` architecture. It is **website-only** and does not change `apps/api` or `core` architecture.
Authoritative contract: [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1).
## 1) Core rule: API owns business truth ## 1) Core rule: API owns business truth
- `apps/api` is the only source of truth for business rules and canonical filtering/sorting. - `apps/api` is the only source of truth for business rules and canonical filtering/sorting.
@@ -42,6 +44,9 @@ It is **website-only** and does not change `apps/api` or `core` architecture.
- Contains only values ready to render (mostly strings/numbers). - Contains only values ready to render (mostly strings/numbers).
- Built from Page DTO (initial render) and from ViewModel (post-hydration). - Built from Page DTO (initial render) and from ViewModel (post-hydration).
The mapping between Page DTO, ViewModel, and ViewData is performed by Presenters.
See [`PRESENTERS.md`](docs/architecture/website/PRESENTERS.md:1).
## 3) Required per-route structure ## 3) Required per-route structure
Every route MUST follow: Every route MUST follow:
@@ -50,6 +55,9 @@ Every route MUST follow:
2) `*PageClient.tsx` (client): builds ViewData and renders Template 2) `*PageClient.tsx` (client): builds ViewData and renders Template
3) `*Template.tsx` (pure UI): renders ViewData only 3) `*Template.tsx` (pure UI): renders ViewData only
All writes enter through Server Actions.
See [`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:1).
## 4) Authoritative specification ## 4) Authoritative specification
This document is an entry point only. This document is an entry point only.
@@ -58,3 +66,6 @@ The authoritative, test-enforced spec lives at:
- [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1) - [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1)
Final contract:
- [`WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1)