Agent skill
server-actions
Next.js Server Actions for mutations in this application. Covers entity actions, user actions, team actions, and best practices. Use this skill when implementing mutations from Client Components.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/server-actions-nextspark-js-nextspark
SKILL.md
Server Actions Skill
Patterns for using Next.js Server Actions to perform mutations from Client Components.
Architecture Overview
core/lib/actions/
├── index.ts # Re-exports all actions
├── types.ts # Shared types (EntityActionResult, etc.)
├── entity.actions.ts # Generic CRUD for any registered entity
├── user.actions.ts # User profile management
└── team.actions.ts # Team and member management
When to Use This Skill
- Calling mutations from Client Components (
'use client') - Form submissions without JavaScript
- Automatic cache revalidation after mutations
- Type-safe end-to-end mutations
Import Pattern
// From Client Components
'use client'
import {
// Entity actions
createEntity,
updateEntity,
deleteEntity,
getEntity,
listEntities,
deleteEntities,
entityExists,
countEntities,
// User actions
updateProfile,
updateAvatar,
deleteAccount,
// Team actions
updateTeam,
inviteMember,
removeMember,
updateMemberRole,
} from '@nextsparkjs/core/actions'
Security Model
All actions follow the same security pattern:
- Auth is obtained from session/cookies (NEVER from client parameters)
userIdcomes fromgetTypedSession()teamIdcomes fromhttpOnlycookieactiveTeamId- Permissions are checked via
checkPermission()
Never pass userId/teamId as parameters:
// WRONG - Never trust client data for auth
export async function updateProfile(userId: string, data: ProfileData) { ... }
// CORRECT - Get auth from server context
export async function updateProfile(data: ProfileData) {
const session = await getTypedSession(await headers())
if (!session?.user?.id) {
return { success: false, error: 'Authentication required' }
}
const userId = session.user.id
// ...
}
Entity Actions
createEntity
Create a new entity record.
const result = await createEntity('products', {
name: 'New Product',
price: 99.99,
status: 'draft',
})
if (result.success) {
console.log('Created:', result.data)
} else {
console.error('Error:', result.error)
}
updateEntity
Update an existing entity.
const result = await updateEntity('products', productId, {
status: 'published',
})
// With custom revalidation
await updateEntity('products', productId, data, {
revalidatePaths: ['/dashboard/overview'],
revalidateTags: ['product-stats'],
})
deleteEntity
Delete an entity by ID.
const result = await deleteEntity('products', productId)
// With redirect after delete
await deleteEntity('products', productId, {
redirectTo: '/dashboard/products',
})
deleteEntities (Batch)
Delete multiple entities at once.
const result = await deleteEntities('products', ['id1', 'id2', 'id3'])
if (result.success) {
console.log(`Deleted ${result.data.deletedCount} products`)
}
getEntity / listEntities
For reads from Client Components (prefer Server Components when possible).
// Get single entity
const result = await getEntity('products', productId)
// List with pagination and filters
const result = await listEntities('products', {
limit: 20,
offset: 0,
where: { status: 'active' },
orderBy: 'createdAt',
orderDir: 'desc',
})
entityExists / countEntities
// Check if entity exists
const result = await entityExists('products', productId)
// Count with filter
const result = await countEntities('products', { status: 'active' })
User Actions
updateProfile
Update the current user's profile.
const result = await updateProfile({
firstName: 'John',
lastName: 'Doe',
timezone: 'America/New_York',
language: 'en',
})
// Allowed fields: firstName, lastName, name, country, timezone, language
updateAvatar
Update the user's avatar (URL, not file upload).
// After uploading file to storage and getting URL
const formData = new FormData()
formData.append('avatar', 'https://example.com/uploaded-avatar.jpg')
const result = await updateAvatar(formData)
deleteAccount
Delete the current user's account.
// Check conditions first!
const result = await deleteAccount()
// Will fail if user owns teams
// Error: "Cannot delete account while owning teams. Transfer ownership first."
Team Actions
updateTeam
Update team information (requires owner or admin role).
const result = await updateTeam(teamId, {
name: 'New Team Name',
description: 'Updated description',
slug: 'new-slug',
})
// Will check slug availability if changing
inviteMember
Add a user to the team (requires owner or admin role).
const result = await inviteMember(
teamId,
'[email protected]', // User must already exist
'member' // 'member' | 'admin' | 'viewer'
)
if (result.success) {
console.log('Invited:', result.data.memberId)
}
removeMember
Remove a member from the team.
const result = await removeMember(teamId, userId)
// Cannot remove:
// - Team owner (must transfer ownership first)
// - Other admins (if requestor is admin, not owner)
updateMemberRole
Change a member's role.
const result = await updateMemberRole(teamId, userId, 'admin')
// Restrictions:
// - Cannot set role to 'owner' (use transferOwnership)
// - Only owner can promote to admin
// - Only owner can demote admins
Result Types
All actions return a discriminated union:
type EntityActionResult<T> =
| { success: true; data: T }
| { success: false; error: string }
type EntityActionVoidResult =
| { success: true }
| { success: false; error: string }
Usage with type guards:
const result = await createEntity('products', data)
if (result.success) {
// TypeScript knows result.data exists
console.log(result.data.id)
} else {
// TypeScript knows result.error exists
toast.error(result.error)
}
Revalidation Patterns
Default Revalidation
Entity actions automatically revalidate:
- Create:
/dashboard/{entitySlug} - Update:
/dashboard/{entitySlug}and/dashboard/{entitySlug}/{id} - Delete:
/dashboard/{entitySlug}
Custom Revalidation
await createEntity('products', data, {
revalidatePaths: ['/dashboard/overview', '/public/catalog'],
revalidateTags: ['product-count', 'catalog'],
})
Redirect After Mutation
await deleteEntity('products', id, {
redirectTo: '/dashboard/products',
})
// Note: redirect() throws NEXT_REDIRECT - action won't return data
Form Integration
With React Hook Form
'use client'
function ProductForm() {
const form = useForm()
async function onSubmit(data) {
const result = await createEntity('products', data)
if (result.success) {
router.push(`/products/${result.data.id}`)
} else {
form.setError('root', { message: result.error })
}
}
return <form onSubmit={form.handleSubmit(onSubmit)}>...</form>
}
With Native Forms
'use client'
function ProfileForm() {
async function handleAction(formData: FormData) {
const result = await updateProfile({
firstName: formData.get('firstName') as string,
lastName: formData.get('lastName') as string,
})
// Handle result
}
return (
<form action={handleAction}>
<input name="firstName" />
<input name="lastName" />
<button type="submit">Save</button>
</form>
)
}
Creating New Server Actions
Template
// core/lib/actions/[domain].actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { headers } from 'next/headers'
import { getTypedSession } from '../auth'
import type { EntityActionResult, EntityActionVoidResult } from './types'
export async function myAction(data: MyInput): Promise<EntityActionResult<MyOutput>> {
try {
// 1. Get auth context
const headersList = await headers()
const session = await getTypedSession(headersList)
if (!session?.user?.id) {
return { success: false, error: 'Authentication required' }
}
const userId = session.user.id
// 2. Validate input
if (!data.requiredField) {
return { success: false, error: 'Required field is missing' }
}
// 3. Check permissions (if needed)
// const hasPermission = await checkPermission(userId, teamId, 'resource.action')
// 4. Execute business logic via Service
const result = await MyService.doSomething(userId, data)
// 5. Revalidate caches
revalidatePath('/dashboard/relevant-path')
return { success: true, data: result }
} catch (error) {
console.error('[myAction] Error:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
Checklist for New Actions
- File marked with
'use server'at top - Auth obtained from session (not parameters)
- Input validation before service calls
- Permission checks where needed
- Business logic delegated to Services
-
revalidatePathcalled for affected routes - Error handling with try/catch
- Proper return type (
EntityActionResultorEntityActionVoidResult) - Export from
index.ts
Testing Server Actions
// tests/jest/lib/actions/my.actions.test.ts
import { myAction } from '@/core/lib/actions/my.actions'
// Mock dependencies
jest.mock('@/core/lib/auth', () => ({
getTypedSession: jest.fn(),
}))
jest.mock('next/cache', () => ({
revalidatePath: jest.fn(),
}))
jest.mock('next/headers', () => ({
headers: jest.fn().mockReturnValue(new Headers()),
}))
describe('myAction', () => {
beforeEach(() => {
jest.clearAllMocks()
// Setup authenticated user
const { getTypedSession } = require('@/core/lib/auth')
getTypedSession.mockResolvedValue({
user: { id: 'user-123' },
})
})
it('succeeds with valid data', async () => {
const result = await myAction({ name: 'Test' })
expect(result.success).toBe(true)
})
it('fails when not authenticated', async () => {
const { getTypedSession } = require('@/core/lib/auth')
getTypedSession.mockResolvedValue(null)
const result = await myAction({ name: 'Test' })
expect(result.success).toBe(false)
expect(result.error).toBe('Authentication required')
})
})
Performance Considerations
- Prefer Server Components for reads - Server Actions are for mutations
- Batch operations when possible - Use
deleteEntitiesinstead of multipledeleteEntity - Minimal revalidation - Only revalidate affected paths
- Early validation - Fail fast before expensive operations
Related Skills
service-layer- Business logic implementationtanstack-query- Optimistic updates and cachingreact-patterns- Form and component patternszod-validation- Input validation schemas
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?