4.3 KiB
4.3 KiB
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:
// 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
// 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<Result<void, string>> {
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<Result<void, string>> {
try {
await this.service.deleteUser(userId);
return ResultFactory.ok(undefined);
} catch (error) {
return ResultFactory.error('DELETE_USER_FAILED');
}
}
}
4) Why This Pattern?
Benefits:
- Framework independence - Mutations can be tested without Next.js
- Consistent pattern - Mirrors PageQueries for reads/writes
- Easy migration - Can switch frameworks without rewriting business logic
- Testable - Can unit test mutations in isolation
- 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
- Server Actions are thin wrappers - They only handle framework concerns (revalidation, redirects)
- Mutations handle infrastructure - They create services, handle errors
- Services handle business logic - They orchestrate API calls
- Mutations are framework-agnostic - No Next.js imports except in tests
- 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<ApiDto, string> |
Result<void, string> |
| Revalidation | N/A | Server Action handles it |
9) Example Flow
Read (RSC):
RSC page.tsx
↓
PageQuery.execute()
↓
Service
↓
API Client
↓
Result<ApiDto, string>
↓
ViewDataBuilder
↓
Template
Write (Server Action):
Client Component
↓
Server Action
↓
Mutation.execute()
↓
Service
↓
API Client
↓
Result<void, string>
↓
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.