@@ -12,66 +12,101 @@ alwaysApply: false
1212- Operations needing cookie/session access
1313- Revalidation of cached pages after a mutation
1414
15- ## RULE 1: Always Validate Inputs with Zod
16- NEVER trust raw FormData. Always parse through a Zod schema first.
15+ ## When to Use Route Handlers (app/api/**)
16+ - Webhooks (Stripe, GitHub — external services calling your API)
17+ - Public API endpoints (no auth needed or API key auth)
18+ - File upload/download streams
19+ - Third-party OAuth callbacks
20+ - CORS-required endpoints
21+
22+ ## RULE 1: Always Use Typed ActionResponse
23+
24+ Every Server Action MUST return `ActionResponse<T>` — never throw, never leak internals.
1725
18- ✅ CORRECT:
1926```typescript
2027'use server'
21-
2228import { z } from 'zod'
2329import { createClient } from '@/lib/supabase/server'
2430import { revalidatePath } from 'next/cache'
2531
26- const createTodoSchema = z.object({
27- title: z.string().min(1).max(200),
32+ type ActionResponse<T = void> =
33+ | { success: true; data: T }
34+ | { success: false; error: string }
35+
36+ const CreateTodoSchema = z.object({
37+ title: z.string().min(1, 'Title required').max(200),
2838})
2939
30- export async function createTodo(formData: FormData) {
31- const parsed = createTodoSchema.safeParse({
32- title: formData.get('title'),
33- })
40+ export async function createTodo(
41+ formData: FormData
42+ ): Promise<ActionResponse<{ id: string }>> {
43+ // 1. Auth first
44+ const supabase = await createClient()
45+ const { data: { user } } = await supabase.auth.getUser()
46+ if (!user) return { success: false, error: 'Unauthorized' }
3447
48+ // 2. Validate
49+ const parsed = CreateTodoSchema.safeParse({ title: formData.get('title') })
3550 if (!parsed.success) {
36- return { error: 'Invalid input', details : parsed.error.flatten() }
51+ return { success: false, error : parsed.error.issues[0].message }
3752 }
3853
39- const supabase = await createClient()
40- const { data: { user } } = await supabase.auth.getUser()
41- if (!user) return { error: 'Unauthorized' }
42-
43- const { error } = await supabase
54+ // 3. Execute
55+ const { data, error } = await supabase
4456 .from('todos')
4557 .insert({ title: parsed.data.title, user_id: user.id })
58+ .select('id')
59+ .single()
4660
47- if (error) return { error: error.message }
61+ if (error) {
62+ console.error('[CREATE_TODO_ERROR]', error) // Server-side only
63+ return { success: false, error: 'Failed to create todo' } // Generic to client
64+ }
4865
4966 revalidatePath('/todos')
50- return { success: true }
67+ return { success: true, data: { id: data.id } }
5168}
5269```
5370
54- ## RULE 2: Return Structured Errors, Don't Throw
55- Server Actions should return `{ error: string }` or `{ success: true }`.
56- Only use `redirect()` for navigation after successful mutations.
71+ ## RULE 2: Never Export Non-Action Values from 'use server' Files
5772
58- ## When to Use Route Handlers (app/api/**)
59- - Webhooks (Stripe, GitHub — external services calling your API)
60- - Public API endpoints (no auth needed or API key auth)
61- - File uploads/download streams
62- - Third-party OAuth callbacks
63- - CORS-required endpoints
73+ Files with `'use server'` export ONLY async functions.
74+ Never export constants, types, or synchronous functions from these files.
75+
76+ ❌ WRONG:
77+ ```typescript
78+ 'use server'
79+ export const MAX_TODOS = 100 // Becomes a server action — causes infinite loops
80+ ```
81+
82+ ✅ CORRECT — types in a separate file:
83+ ```typescript
84+ // src/types/actions.ts (NOT a server action file)
85+ export type ActionResponse<T = void> =
86+ | { success: true; data: T }
87+ | { success: false; error: string }
88+ ```
89+
90+ ## RULE 3: Always try/catch in Route Handlers
6491
65- ## RULE 3: Always Add try/catch in Route Handlers
6692```typescript
6793export async function POST(request: Request) {
6894 try {
6995 const body = await request.json()
70- // ... logic
7196 return Response.json({ success: true })
7297 } catch (error) {
73- console.error('API Error: ', error)
98+ console.error('[API_ERROR] ', error)
7499 return Response.json({ error: 'Internal server error' }, { status: 500 })
75100 }
76101}
77102```
103+
104+ ## RULE 4: revalidatePath After Every Mutation
105+
106+ After any successful database write, call `revalidatePath()` on the affected route.
107+ Without this, the UI shows stale cached data until the next full page reload.
108+
109+ ```typescript
110+ revalidatePath('/dashboard/posts') // The list page
111+ revalidatePath(`/posts/${postId}`) // The individual item page if public
112+ ```
0 commit comments