# Mutations (Strict) This document defines the **Mutation** pattern for `apps/website`. Mutations exist to provide framework-agnostic write operations. ## 1) Definition A **Mutation** is a framework-agnostic operation that orchestrates writes. Mutations are the write equivalent of PageQueries. ## 2) Relationship to Next.js Server Actions **Server Actions are the entry point**, but they should be thin wrappers: ```typescript // app/admin/actions.ts (Next.js framework code) 'use server'; import { AdminUserMutation } from '@/lib/mutations/admin/AdminUserMutation'; import { revalidatePath } from 'next/cache'; export async function updateUserStatus(userId: string, status: string) { const mutation = new AdminUserMutation(); const result = await mutation.updateUserStatus(userId, status); if (result.isErr()) { console.error('updateUserStatus failed:', result.getError()); throw new Error('Failed to update user status'); } revalidatePath('/admin/users'); } ``` ## 3) Mutation Structure ```typescript // lib/mutations/admin/AdminUserMutation.ts import { Result, ResultFactory } from '@/lib/contracts/Result'; export class AdminUserMutation { private service: AdminService; constructor() { // Manual DI for serverless const logger = new ConsoleLogger(); const errorReporter = new EnhancedErrorReporter(logger, { showUserNotifications: true, logToConsole: true, reportToExternal: process.env.NODE_ENV === 'production', }); const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; const apiClient = new AdminApiClient(baseUrl, errorReporter, logger); this.service = new AdminService(apiClient); } async updateUserStatus(userId: string, status: string): Promise> { try { await this.service.updateUserStatus(userId, status); return ResultFactory.ok(undefined); } catch (error) { return ResultFactory.error('UPDATE_USER_STATUS_FAILED'); } } async deleteUser(userId: string): Promise> { try { await this.service.deleteUser(userId); return ResultFactory.ok(undefined); } catch (error) { return ResultFactory.error('DELETE_USER_FAILED'); } } } ``` ## 4) Why This Pattern? **Benefits:** 1. **Framework independence** - Mutations can be tested without Next.js 2. **Consistent pattern** - Mirrors PageQueries for reads/writes 3. **Easy migration** - Can switch frameworks without rewriting business logic 4. **Testable** - Can unit test mutations in isolation 5. **Reusable** - Can be called from other contexts (cron jobs, etc.) ## 5) Naming Convention - Mutations: `*Mutation.ts` - Server Actions: `actions.ts` (thin wrappers) ## 6) File Structure ``` lib/ mutations/ admin/ AdminUserMutation.ts AdminLeagueMutation.ts league/ LeagueJoinMutation.ts team/ TeamUpdateMutation.ts ``` ## 7) Non-negotiable Rules 1. **Server Actions are thin wrappers** - They only handle framework concerns (revalidation, redirects) 2. **Mutations handle infrastructure** - They create services, handle errors 3. **Services handle business logic** - They orchestrate API calls 4. **Mutations are framework-agnostic** - No Next.js imports except in tests 5. **Mutations must be deterministic** - Same inputs = same outputs ## 8) Comparison with PageQueries | Aspect | PageQuery | Mutation | |--------|-----------|----------| | Purpose | Read data | Write data | | Location | `lib/page-queries/` | `lib/mutations/` | | Framework | Called from RSC | Called from Server Actions | | Infrastructure | Manual DI | Manual DI | | Returns | `Result` | `Result` | | Revalidation | N/A | Server Action handles it | ## 9) Example Flow **Read (RSC):** ``` RSC page.tsx ↓ PageQuery.execute() ↓ Service ↓ API Client ↓ Result ↓ ViewDataBuilder ↓ Template ``` **Write (Server Action):** ``` Client Component ↓ Server Action ↓ Mutation.execute() ↓ Service ↓ API Client ↓ Result ↓ Revalidation ``` ## 10) Enforcement ESLint rules should enforce: - Server Actions must call Mutations (not Services directly) - Mutations must not import Next.js (except in tests) - Mutations must use services See `docs/architecture/website/WEBSITE_GUARDRAILS.md` for details.