Skip to content

Commit 7160e4c

Browse files
committed
fix: server-actions.mdc — patch error.message leakage, add ActionResponse type, RULE 2 (no non-action exports from use server), RULE 4 (revalidatePath guidance)
1 parent 56e571e commit 7160e4c

1 file changed

Lines changed: 65 additions & 30 deletions

File tree

.cursor/rules/server-actions.mdc

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
2228
import { z } from 'zod'
2329
import { createClient } from '@/lib/supabase/server'
2430
import { 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
6793
export 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

Comments
 (0)