372 lines
9.7 KiB
Markdown
372 lines
9.7 KiB
Markdown
# 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 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.
|
|
- **Uncle Bob says**: "Data structures should not have behavior."
|
|
|
|
See [`VIEW_DATA.md`](docs/architecture/website/VIEW_DATA.md:1).
|
|
|
|
Canonical placement in this repo:
|
|
|
|
- `apps/website/templates/**` (Templates that accept ViewData only)
|
|
|
|
### 3.3 ViewModel
|
|
|
|
Definition: the client-only, UI-owned class representing fully prepared UI state.
|
|
|
|
Rules:
|
|
|
|
- Instantiated only in `'use client'` modules.
|
|
- Never serialized.
|
|
- Used for client components that need state management.
|
|
- **Uncle Bob says**: "Objects expose behavior, not data."
|
|
|
|
See [`VIEW_MODELS.md`](docs/architecture/website/VIEW_MODELS.md:1).
|
|
|
|
Canonical placement in this repo:
|
|
|
|
- `apps/website/lib/view-models/**`
|
|
|
|
## 4) Data transformation helpers (strict)
|
|
|
|
### 4.1 ViewModel Builder
|
|
|
|
Definition: transforms API Transport DTOs into ViewModels.
|
|
|
|
Purpose: prepare raw API data for client-side state management.
|
|
|
|
Rules:
|
|
|
|
- MUST be deterministic.
|
|
- MUST be side-effect free.
|
|
- MUST NOT call HTTP.
|
|
- MUST NOT call the API.
|
|
- Input: `Result<ApiDto, string>` or `ApiDto`
|
|
- Output: ViewModel
|
|
- MUST use `static build()` and `satisfies ViewModelBuilder`.
|
|
|
|
See [`BUILDERS.md`](docs/architecture/website/BUILDERS.md:1).
|
|
|
|
Canonical placement in this repo:
|
|
|
|
- `apps/website/lib/builders/view-models/**`
|
|
|
|
### 4.2 ViewData Builder
|
|
|
|
Definition: transforms API DTOs directly into ViewData for templates.
|
|
|
|
Purpose: prepare API data for server-side rendering.
|
|
|
|
Rules:
|
|
|
|
- MUST be deterministic.
|
|
- MUST be side-effect free.
|
|
- MUST NOT call HTTP.
|
|
- MUST NOT call the API.
|
|
- Input: `Result<ApiDto, string>` or `ApiDto`
|
|
- Output: ViewData
|
|
- MUST use `static build()` and `satisfies ViewDataBuilder`.
|
|
|
|
See [`BUILDERS.md`](docs/architecture/website/BUILDERS.md:1).
|
|
|
|
Canonical placement in this repo:
|
|
|
|
- `apps/website/lib/builders/view-data/**`
|
|
|
|
### 4.3 Result Type
|
|
|
|
Definition: Type-safe error handling for all operations.
|
|
|
|
Purpose: eliminate exceptions and provide explicit error paths.
|
|
|
|
Rules:
|
|
|
|
- All PageQueries return `Result<ViewData, PresentationError>`
|
|
- All Mutations return `Result<void, MutationError>`
|
|
- All Services return `Result<ApiDto, DomainError>`
|
|
- Use `Result.ok(value)` for success
|
|
- Use `Result.err(error)` for errors
|
|
- Never throw exceptions
|
|
|
|
See [`Result.ts`](apps/website/lib/contracts/Result.ts:1).
|
|
|
|
Canonical placement in this repo:
|
|
|
|
- `apps/website/lib/contracts/Result.ts`
|
|
|
|
### 4.4 Formatter & Display Object
|
|
|
|
Definition: deterministic, reusable, UI-only formatting/mapping logic.
|
|
|
|
Rules:
|
|
|
|
- **Formatters**: Stateless utilities for server-side primitive output.
|
|
- **Display Objects**: Rich Value Objects for client-side interactive APIs.
|
|
- MUST NOT call `Intl.*` or `toLocale*` (unless client-only).
|
|
- MUST NOT implement business rules.
|
|
|
|
See [`FORMATTERS.md`](docs/architecture/website/FORMATTERS.md:1).
|
|
|
|
Canonical placement in this repo:
|
|
|
|
- `apps/website/lib/display-objects/**`
|
|
|
|
## 5) Read flow (strict)
|
|
|
|
### Server Components (RSC)
|
|
```text
|
|
RSC page.tsx
|
|
↓
|
|
PageQuery (manual construction)
|
|
↓
|
|
Service (creates own API Client, Logger, ErrorReporter)
|
|
↓
|
|
API Client (makes HTTP calls)
|
|
↓
|
|
API Transport DTO
|
|
↓
|
|
Result<ApiDto, DomainError>
|
|
↓
|
|
PageQuery (maps DomainError → PresentationError)
|
|
↓
|
|
Result<ViewData, PresentationError>
|
|
↓
|
|
ViewData Builder (lib/builders/view-data/)
|
|
↓
|
|
ViewData
|
|
↓
|
|
Template
|
|
```
|
|
|
|
**Key Points:**
|
|
- PageQuery constructs Service
|
|
- Service creates its own dependencies
|
|
- Service returns Result<ApiDto, DomainError>
|
|
- PageQuery maps errors to presentation layer
|
|
- Builder transforms API DTO to ViewData
|
|
|
|
### Client Components
|
|
```text
|
|
Client Component
|
|
↓
|
|
API client (useEffect)
|
|
↓
|
|
API Transport DTO
|
|
↓
|
|
Result<ApiDto, string>
|
|
↓
|
|
ViewModel Builder (lib/builders/view-models/)
|
|
↓
|
|
ViewModel (lib/view-models/)
|
|
↓
|
|
Client State (useState)
|
|
↓
|
|
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 a mutation** (not services directly)
|
|
- mutation orchestrates services for writes
|
|
|
|
**Server Actions must use Mutations:**
|
|
|
|
```typescript
|
|
// ❌ WRONG - Direct service usage
|
|
'use server';
|
|
import { AdminService } from '@/lib/services/admin/AdminService';
|
|
|
|
export async function updateUserStatus(userId: string, status: string) {
|
|
const service = new AdminService();
|
|
await service.updateUserStatus(userId, status); // ❌ Should use mutation
|
|
}
|
|
|
|
// ✅ CORRECT - Mutation usage
|
|
'use server';
|
|
import { UpdateUserStatusMutation } from '@/lib/mutations/UpdateUserStatusMutation';
|
|
import { revalidatePath } from 'next/cache';
|
|
|
|
export async function updateUserStatus(input: UpdateUserStatusInput) {
|
|
const mutation = new UpdateUserStatusMutation();
|
|
const result = await mutation.execute(input);
|
|
|
|
if (result.isErr()) {
|
|
console.error('updateUserStatus failed:', result.error);
|
|
return { success: false, error: result.error };
|
|
}
|
|
|
|
revalidatePath('/admin/users');
|
|
return { success: true };
|
|
}
|
|
```
|
|
|
|
**Pattern**:
|
|
1. **Server Action** (thin wrapper) - handles framework concerns (revalidation, returns to client)
|
|
2. **Mutation** (framework-agnostic) - constructs Service, calls service methods
|
|
3. **Service** - constructs own dependencies (API Client, Logger), returns Result
|
|
4. **API Client** (infrastructure) - makes HTTP requests, throws HTTP errors
|
|
5. **Result** - type-safe error handling at every layer
|
|
|
|
**Dependency Flow**:
|
|
```
|
|
Server Action
|
|
↓ (constructs)
|
|
Mutation
|
|
↓ (constructs)
|
|
Service (creates API Client, Logger, ErrorReporter)
|
|
↓ (calls)
|
|
API Client
|
|
↓ (HTTP)
|
|
Backend API
|
|
```
|
|
|
|
**Rationale**:
|
|
- Mutations are framework-agnostic (can be tested without Next.js)
|
|
- Consistent pattern with PageQueries
|
|
- Type-safe error handling with Result
|
|
- Makes infrastructure explicit and testable
|
|
- Manual construction (no DI container issues)
|
|
|
|
See [`MUTATIONS.md`](docs/architecture/website/MUTATIONS.md:1) and [`SERVICES.md`](docs/architecture/website/SERVICES.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 [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1) and [`docs/architecture/website/BLOCKERS.md`](docs/architecture/website/BLOCKERS.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. Templates accept ViewData only.
|
|
5. Page Queries do not format; they only compose.
|
|
6. All operations return `Result<T, E>` for type-safe error handling.
|
|
7. ViewData Builders transform API DTO → ViewData (RSC).
|
|
8. ViewModel Builders transform API DTO → ViewModel (Client).
|
|
9. Builders are pure and deterministic.
|
|
10. Server Actions are the only write entry point.
|
|
11. Server Actions must use Mutations (not Services directly).
|
|
12. Mutations orchestrate Services for writes.
|
|
13. Authorization always belongs to the API. |