3.8 KiB
3.8 KiB
Write Flow Update (Mutation Pattern)
This document updates the write flow section of WEBSITE_CONTRACT.md to use the Mutation pattern.
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 creates infrastructure and calls Service
- Service orchestrates API calls and business logic
Server Actions must use Mutations
// ❌ 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 { 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');
}
Mutation Pattern
// 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');
}
}
}
Flow
- Server Action (thin wrapper) - handles framework concerns (revalidation, redirects)
- Mutation (framework-agnostic) - creates infrastructure, calls service
- Service (business logic) - orchestrates API calls
- API Client (infrastructure) - makes HTTP requests
- Result - type-safe error handling
Rationale
- Framework independence: Mutations can be tested without Next.js
- Consistency: Mirrors PageQuery pattern for reads/writes
- Type-safe errors: Result pattern eliminates exceptions
- Migration ease: Can switch frameworks without rewriting business logic
- Testability: Can unit test mutations in isolation
- Reusability: Can be called from other contexts (cron jobs, etc.)
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 |
See Also
MUTATIONS.md- Full mutation pattern documentationSERVICES.md- Service layer documentationWEBSITE_CONTRACT.md- Main contract