Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add soilmass/vibe-coding-plugin --skill "react-server-actions"
Install specific skill from multi-skill repository
# Description
>
# SKILL.md
name: react-server-actions
description: >
React 19 Server Actions — "use server" directive, Zod validation, auth checks, revalidation, error handling, FormData processing
allowed-tools: Read, Grep, Glob
React Server Actions
Purpose
Server Action patterns for React 19 and Next.js 15. Covers the "use server" directive,
validation, auth, and revalidation. The ONE skill for server-side mutations.
When to Use
- Processing form submissions on the server
- Mutating database data from React components
- Implementing create/update/delete operations
- Triggering cache revalidation after mutations
When NOT to Use
- Client-side form UI →
react-forms - Read-only data fetching →
nextjs-data - External API endpoints →
api-routes
Pattern
Complete Server Action
// src/actions/createPost.ts
"use server";
import { z } from "zod";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
});
type ActionState = {
success?: boolean;
error?: Record<string, string[]>;
};
export async function createPost(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
// 1. Auth check
const session = await auth();
if (!session?.user) {
return { error: { _form: ["Unauthorized"] } };
}
// 2. Validate input
const parsed = CreatePostSchema.safeParse({
title: formData.get("title"),
content: formData.get("content"),
});
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors };
}
// 3. Mutate database
await db.post.create({
data: { ...parsed.data, authorId: session.user.id },
});
// 4. Revalidate cache
revalidatePath("/posts");
return { success: true };
}
Inline Server Action
export default async function Page() {
async function deletePost(formData: FormData) {
"use server";
const id = formData.get("id") as string;
await db.post.delete({ where: { id } });
revalidatePath("/posts");
}
return (
<form action={deletePost}>
<input type="hidden" name="id" value="123" />
<button type="submit">Delete</button>
</form>
);
}
Error classification pattern
// src/lib/action-errors.ts
type ErrorCategory = "validation" | "auth" | "external" | "system";
export function classifyError(error: unknown): {
category: ErrorCategory;
message: string;
} {
if (error instanceof z.ZodError) {
return { category: "validation", message: "Invalid input" };
}
if (error instanceof AuthError) {
return { category: "auth", message: "Authentication required" };
}
if (error instanceof FetchError || error instanceof TimeoutError) {
return { category: "external", message: "Service temporarily unavailable" };
}
return { category: "system", message: "Something went wrong" };
}
// Usage in Server Action
export async function myAction(prevState: ActionState, formData: FormData) {
try {
// ... action logic
} catch (error) {
const { category, message } = classifyError(error);
if (category === "system") logger.error(error); // Only log unexpected errors
return { error: { _form: [message] } };
}
}
redirect/revalidate OUTSIDE try-catch
"use server";
export async function updatePost(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const session = await auth();
if (!session?.user) return { error: { _form: ["Unauthorized"] } };
const parsed = UpdatePostSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { error: parsed.error.flatten().fieldErrors };
try {
await db.post.update({
where: { id: parsed.data.id, authorId: session.user.id },
data: parsed.data,
});
} catch (e) {
return { error: { _form: ["Update failed"] } };
}
// redirect() throws NEXT_REDIRECT internally — if inside try-catch,
// the catch block intercepts it and navigation breaks silently.
// revalidateTag() throws NEXT_REVALIDATE — same issue.
revalidateTag("posts");
redirect("/posts");
}
Anti-pattern
// WRONG: no validation or auth in Server Action
"use server";
export async function updateUser(formData: FormData) {
const name = formData.get("name") as string;
// No auth check — anyone can call this!
// No input validation — SQL injection risk!
await db.user.update({
where: { id: formData.get("id") as string },
data: { name },
});
}
Server Actions are public HTTP endpoints. Always validate input and check auth.
Common Mistakes
- Skipping auth checks — Server Actions are publicly accessible endpoints
- No Zod validation — trusting FormData directly
- Using
redirect()inside try-catch — it throws NEXT_REDIRECT - Forgetting
revalidatePath/revalidateTag— stale data after mutation - Returning sensitive error details to client
Checklist
- [ ]
"use server"directive at top of file or function - [ ] Auth check before any mutation
- [ ] Zod schema validates all FormData inputs
- [ ]
revalidatePathorrevalidateTagafter mutation - [ ] Error state returned, not thrown (for form error display)
Composes With
react-forms— forms provide the UI, actions process the dataprisma— actions call Prisma for database mutationscaching— actions trigger cache revalidation
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.