dist/plugins/web-framework-nextjs-server-actions/skills/web-framework-nextjs-server-actions/SKILL.md
Server Actions patterns for mutations, revalidation, and form handling in Next.js App Router
npx skillsauth add agents-inc/skills web-framework-nextjs-server-actionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use Server Actions for mutations (create, update, delete) in Next.js App Router. Define with
'use server'directive, invoke from forms or event handlers, revalidate cache after mutations.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST add 'use server' directive at the top of the file OR at the top of the async function)
(You MUST revalidate the cache after mutations using revalidatePath() or revalidateTag())
(You MUST validate all input data on the server - client-side validation is NOT sufficient for security)
(You MUST perform authorization checks inside EVERY Server Action - they are public HTTP endpoints)
(You MUST call revalidatePath() BEFORE redirect() to ensure fresh data)
</critical_requirements>
Auto-detection: Server Actions, use server directive, revalidatePath, revalidateTag, formAction, useActionState, useFormStatus, useOptimistic, server mutation
When to use:
Key patterns covered:
'use server' directive)React 19 Integration:
useActionState, useFormStatus, and useOptimistic are React 19 hooks (not Next.js-specific)useActionState replaces the deprecated ReactDOM.useFormState from React CanaryWhen NOT to use:
Detailed Resources:
Server Actions are asynchronous functions that execute on the server, invoked via network requests from the client. They integrate with Next.js caching and revalidation, enabling single-roundtrip updates where both UI and data refresh together.
Core Principles:
Server Actions vs API Routes:
Server Actions are ideal for form submissions and mutations tightly coupled to UI. Use Route Handlers for external API consumers, webhooks, or complex multi-step operations requiring parallel execution.
</philosophy>Define Server Actions using the 'use server' directive. Place it at the file level (recommended for Client Component imports) or function level (for Server Components).
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
// Mutation logic here (defer to your database solution)
// ...
revalidatePath("/posts");
}
Why good: File-level directive marks all exports as Server Actions, clear separation of server code, can be imported into Client Components
// app/page.tsx - Server Component
export default function Page() {
async function createPost(formData: FormData) {
'use server'
// Server Action logic
}
return <form action={createPost}>...</form>
}
Why good: Inline definition when action is only used in one place, directive at function level keeps action close to usage
Invoke Server Actions via the action attribute on forms. This enables progressive enhancement - forms work even without JavaScript.
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input type="text" name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
)
}
Why good: Form works without JavaScript (progressive enhancement), FormData automatically passed to action, native browser form validation works
When to use: Any form that performs a mutation - this is the primary invocation pattern for Server Actions.
After mutations, revalidate the cache to reflect changes in the UI. Use revalidatePath() for specific routes or revalidateTag() for tagged data.
"use server";
import { revalidatePath } from "next/cache";
export async function updatePost(id: string, formData: FormData) {
// Update in database...
// Revalidate the posts list page
revalidatePath("/posts");
// Revalidate the specific post page
revalidatePath(`/posts/${id}`);
}
Why good: Invalidates cached data for specific routes, UI shows fresh data after mutation
"use server";
import { revalidateTag } from "next/cache";
export async function createPost(formData: FormData) {
// Create in database...
// Invalidate all data tagged with 'posts'
revalidateTag("posts");
}
Why good: Invalidates all cached data with a specific tag, useful when multiple routes display the same data
When to use: Use revalidatePath() for route-specific invalidation, revalidateTag() for cross-route invalidation of tagged data.
Redirect users after successful mutations. Call revalidatePath() BEFORE redirect() to ensure the destination shows fresh data.
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
// Create in database...
// Revalidate BEFORE redirect
revalidatePath("/posts");
// Redirect to posts list
redirect("/posts");
}
Why good: Ensures destination page shows updated data, redirect() throws internally so nothing runs after it
Pass arguments beyond FormData using JavaScript's bind() method.
// actions.ts
"use server";
export async function updateUser(userId: string, formData: FormData) {
const name = formData.get("name") as string;
// Update user with userId...
}
// components/user-form.tsx
'use client'
import { updateUser } from '@/app/actions'
export function UserForm({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update</button>
</form>
)
}
Why good: Passes additional context (IDs, etc.) to Server Actions, works with progressive enhancement, type-safe with TypeScript
Invoke Server Actions from event handlers when not using forms.
// components/like-button.tsx
'use client'
import { useState, useTransition } from 'react'
import { incrementLike } from '@/app/actions'
export function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
const [isPending, startTransition] = useTransition()
const handleClick = () => {
startTransition(async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
})
}
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? 'Liking...' : `Likes: ${likes}`}
</button>
)
}
Why good: startTransition prevents UI blocking during action execution, pending state provides user feedback, works for non-form interactions
When to use: Toggle buttons, quick actions, any mutation not tied to a form submission.
Validate all input data on the server. Use a validation library for type-safe parsing.
// app/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
const CreatePostSchema = z.object({
title: z.string().min(1, "Title is required").max(200),
content: z.string().min(1, "Content is required"),
});
export async function createPost(formData: FormData) {
const rawData = {
title: formData.get("title"),
content: formData.get("content"),
};
const validatedFields = CreatePostSchema.safeParse(rawData);
if (!validatedFields.success) {
return {
success: false,
errors: validatedFields.error.flatten().fieldErrors,
};
}
// Proceed with mutation using validatedFields.data
// ...
revalidatePath("/posts");
return { success: true };
}
Why good: Server-side validation cannot be bypassed, type-safe data after validation, structured error response for UI feedback
Server Actions are public HTTP endpoints. Perform authorization checks inside EVERY action.
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function deletePost(postId: string) {
// Defer to your authentication solution for user/session retrieval
const user = await getCurrentUser();
if (!user) {
throw new Error("Unauthorized: Not authenticated");
}
// Defer to your database solution for fetching the post
const post = await getPost(postId);
if (!post) {
throw new Error("Not found");
}
if (post.authorId !== user.id) {
throw new Error("Forbidden: Not the author");
}
// Proceed with deletion
// ...
revalidatePath("/posts");
}
Why good: Every action verifies identity and permissions, prevents unauthorized access even if action ID is discovered, follows defense-in-depth principle
</patterns>Server Actions are framework-agnostic for business logic. They receive data, perform mutations, and revalidate cache. The actual database operations, authentication, and form UI are handled by other parts of your stack.
Works with:
action attributeuseActionState, useFormStatus, useOptimistic for UI statesrevalidatePath() and revalidateTag()redirect() for post-mutation navigationDefers to:
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST add 'use server' directive at the top of the file OR at the top of the async function)
(You MUST revalidate the cache after mutations using revalidatePath() or revalidateTag())
(You MUST validate all input data on the server - client-side validation is NOT sufficient for security)
(You MUST perform authorization checks inside EVERY Server Action - they are public HTTP endpoints)
(You MUST call revalidatePath() BEFORE redirect() to ensure fresh data)
Failure to follow these rules will cause stale UI, security vulnerabilities, or broken redirects.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety