server-actions

Create Next.js Server Actions with TypeScript following best practices for validation, error handling, auth, and React integration. Use this skill whenever writing or modifying Server Actions in a Next.js project — form submissions, data mutations, CRUD operations, auth-gated actions, redirect flows. Trigger even when the user says "create a server action", "add form handling", "handle form submit on the server", "validate form with Zod", "add server-side mutation", or asks about useActionState / useTransition with actions. Targets Next.js 15/16+ with App Router.

Server Actions Skill

Create Next.js Server Actions with TypeScript following best practices for validation, error handling, and React integration.

Reference Files:

First Step: Read Project Context

IMPORTANT: Before creating server actions, check project configuration files:

.claude/project-context.md for:

  • Authentication provider (Clerk, NextAuth, JWT)
  • Database patterns (tuple error handling, ServiceError class)
  • Logging conventions

CLAUDE.md for:

  • Server Actions patterns (ActionResponse type)
  • Database error handling (ServiceError class)
  • Toast notification system
  • File organization conventions

Context

Server Actions are for mutations only. For data fetching, use Server Components with direct DB calls — that's the idiomatic App Router pattern (no HTTP round-trip, simpler, better performance). Server Actions are invoked via POST requests; they are not the right tool for reads.

This skill helps you create:

  • Form actions - Handle form submissions with validation
  • CRUD mutations - Create, update, delete operations
  • Redirect flows - Multi-step wizards, onboarding
  • Data mutations - Any server-side data changes

Instructions

When the user requests a server action:

1. Analyze Requirements

Gather information about:

  • What data does the action handle?
  • Does it return data or redirect?
  • What validation is needed?
  • What authentication/authorization is required?
  • What errors need to be handled?
  • What cache paths need revalidation?

2. Choose Response Type

ScenarioTypeDescription
Returns dataActionResponse<T>Action returns data to client
Redirects on successRedirectActionAction navigates to new page
Form with useActionStateModified signatureAccepts previousState as first param

3. Create Server Action

File Location: features/[feature]/server/actions/[action-name].ts

Standard Template:

"use server";

import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { auth } from "@clerk/nextjs/server"; // or your auth provider
import { createLogger } from "~/lib/logger";
import type { ActionResponse, RedirectAction } from "~/lib/action-types";

const logger = createLogger({ module: "[feature]-actions" });

export async function actionName(data: InputType): ActionResponse<OutputType> {
  // 1. Authentication
  const { userId } = await auth();
  if (!userId) {
    return { success: false, error: "Authentication required" };
  }

  // 2. Validation
  const parsed = schema.safeParse(data);
  if (!parsed.success) {
    return {
      success: false,
      error: "Validation failed",
      fieldErrors: parsed.error.flatten().fieldErrors,
    };
  }

  // 3. Database operation
  const [error, result] = await dbOperation(parsed.data);
  if (error) {
    logger.error({ userId, error }, "Operation failed");
    return { success: false, error: "Operation failed" };
  }

  // 4. Revalidate cache
  revalidatePath("/affected-path");

  // 5. Return response
  logger.info({ userId, resultId: result.id }, "Operation succeeded");
  return { success: true, data: result };
}

4. Create Validation Schema

File Location: features/[feature]/schemas/[schema-name].ts

import { z } from "zod";

export const inputSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Invalid email address"),
});

export type InputData = z.infer<typeof inputSchema>;

5. Integrate with React Component

Choose integration pattern based on form complexity:

ComplexityPatternUse When
SimpleuseActionStateNative form, simple state
ComplexReact Hook Form + useTransitionDynamic fields, complex validation
RedirectBound action propMulti-step flows

See hooks.md and react-hook-form.md for implementation details.

Core Patterns

Error Handling

Use tuple pattern for database operations:

const [error, data] = await dbOperation();
if (error) {
  if (error.isNotFound) return { success: false, error: "Not found" };
  if (error.isRetryable) return { success: false, error: "Please try again" };
  return { success: false, error: "Operation failed" };
}

Cache Revalidation

Always revalidate after mutations:

revalidatePath("/posts"); // Specific path
revalidateTag("posts"); // By cache tag

Toast Notifications

Server-side toast (setToastCookie) — ONLY with redirect():

Toast cookie is set before redirect(), then the new page reads the cookie and shows the toast. Do NOT use setToastCookie without redirect() — it will not work reliably and breaks the intended pattern.

import { setToastCookie } from "~/lib/toast/server/toast.cookie";

// ✅ Correct: toast + redirect
await setToastCookie("Saved successfully!", "success");
redirect("/dashboard");

Client-side toast — for ALL non-redirect actions:

Action returns message/error in the response, client handles the toast:

// Server action returns feedback via response
return { success: true, data: profile, message: "Profile updated!" };

// Client component handles toast
const result = await updateProfile(data);
if (result.success) {
  toast.success(result.message);
} else {
  toast.error(result.error);
}

Security Checklist

  1. ✅ Verify authentication (await auth())
  2. ✅ Check authorization (ownership, permissions)
  3. ✅ Validate all inputs (Zod schema)
  4. ✅ Log important operations
  5. ✅ Never expose internal errors to client

File Organization

features/
  users/
    server/
      actions/
        create-user.ts
        update-user.ts
        delete-user.ts
        index.ts         # Re-exports
      db/
        users.ts
    schemas/
      user.ts
    types/
      user.ts
    components/
      user-form.tsx

Running and Testing

# Type check
npm run type-check

# Run related tests
npm run test -- features/[feature]

# Test in Storybook (if form component)
npm run storybook:dev

Questions to Ask

When creating server actions:

  • What data does this action create/update/delete?
  • Does it return data or redirect to another page?
  • What validation rules apply to the input?
  • What authentication/authorization is needed?
  • What error cases should be handled?
  • What paths/tags need cache revalidation?
  • Should there be toast notification feedback?
  • Is this part of a multi-step flow?