nextjs-server-actions
1
总安装量
1
周安装量
#50819
全站排名
安装命令
npx skills add https://github.com/gilbertopsantosjr/fullstacknextjs --skill nextjs-server-actions
Agent 安装分布
cursor
1
claude-code
1
Skill 文档
Next.js with ZSA Server Actions
Build type-safe, validated server actions in Next.js with Zod.
Installation
npm install zsa zsa-react zod
# Optional: for React Query integration
npm install zsa-react-query @tanstack/react-query
Basic Server Action
// actions/user.ts
"use server";
import { createServerAction } from "zsa";
import z from "zod";
export const createUserAction = createServerAction()
.input(
z.object({
email: z.string().email(),
name: z.string().min(2),
})
)
.handler(async ({ input }) => {
// Input is fully typed and validated
const user = await db.user.create({
data: { email: input.email, name: input.name },
});
return user;
});
Calling Server Actions
From Server (no try/catch needed)
const [data, err] = await createUserAction({ email: "john@example.com", name: "John Doe" });
if (err) console.error(err.code, err.message);
From Client with useServerAction
"use client";
import { useServerAction } from "zsa-react";
import { createUserAction } from "./actions/user";
export function CreateUserForm() {
const { isPending, execute, data, error, isError, isSuccess, reset } =
useServerAction(createUserAction);
const handleSubmit = async (formData: FormData) => {
const [data, err] = await execute({
email: formData.get("email") as string,
name: formData.get("name") as string,
});
if (err) {
// Error handling
return;
}
// Success handling
};
return (
<form action={handleSubmit}>
<input name="email" type="email" disabled={isPending} />
<input name="name" type="text" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create User"}
</button>
{isError && <p className="error">{error.message}</p>}
{isSuccess && <p className="success">User created: {data.name}</p>}
</form>
);
}
Input & Output Validation
"use server";
import { createServerAction } from "zsa";
import z from "zod";
// Input schema
const createPostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(10),
published: z.boolean().default(false),
tags: z.array(z.string()).optional(),
});
// Output schema
const postOutputSchema = z.object({
id: z.string(),
title: z.string(),
createdAt: z.date(),
});
export const createPostAction = createServerAction()
.input(createPostSchema)
.output(postOutputSchema) // Validates return value
.handler(async ({ input }) => {
const post = await db.post.create({ data: input });
return {
id: post.id,
title: post.title,
createdAt: post.createdAt,
};
});
FormData Input
"use server";
export const submitContactForm = createServerAction()
.input(z.object({ name: z.string().min(2), email: z.string().email() }),
{ type: "formData" })
.handler(async ({ input }) => {
await sendEmail(input);
return { success: true };
});
Procedures (Authentication & Authorization)
Create reusable middleware for auth, roles, and permissions:
// lib/procedures.ts
"use server";
// Authentication procedure
export const authedProcedure = createServerActionProcedure().handler(async () => {
const session = await auth();
if (!session?.user) throw new Error("Not authenticated");
return { user: { id: session.user.id, email: session.user.email, role: session.user.role } };
});
// Admin procedure (chains from authedProcedure)
export const adminProcedure = createServerActionProcedure(authedProcedure)
.handler(async ({ ctx }) => {
if (ctx.user.role !== "admin") throw new Error("Admin access required");
return ctx;
});
Usage:
// Protected action
export const createPost = authedProcedure
.createServerAction()
.input(z.object({ title: z.string() }))
.handler(async ({ input, ctx }) => {
return db.post.create({ data: { ...input, authorId: ctx.user.id } });
});
// Public action (no procedure)
export const publicAction = createServerAction()
.input(schema)
.handler(async ({ input }) => { /* ... */ });
Callbacks
"use server";
import { createServerAction } from "zsa";
import z from "zod";
export const createOrderAction = createServerAction()
.input(z.object({ productId: z.string(), quantity: z.number() }))
.onStart(async () => {
console.log("Order creation started");
})
.onSuccess(async ({ input, data }) => {
// Send confirmation email
await sendOrderConfirmation(data.id);
})
.onError(async ({ err }) => {
// Log error to monitoring service
await logError(err);
})
.onComplete(async () => {
console.log("Order action completed");
})
.handler(async ({ input }) => {
return db.order.create({ data: input });
});
Error Handling
Error Codes: INPUT_PARSE_ERROR, OUTPUT_PARSE_ERROR, ERROR, NOT_AUTHORIZED, TIMEOUT, INTERNAL_SERVER_ERROR
const [result, err] = await execute({ /* ... */ });
if (err) {
switch (err.code) {
case "INPUT_PARSE_ERROR":
console.log(err.fieldErrors); // { email: ["Invalid email"] }
break;
case "NOT_AUTHORIZED":
router.push("/login");
break;
default:
toast.error(err.message);
}
return;
}
// Success - use result
Server-side:
.handler(async ({ input, ctx }) => {
const result = await Service.create(ctx.userId, input);
if (!result.success) throw new Error(result.error);
return result.data;
})
useServerAction Options
const {
data,
isPending,
isOptimistic,
isError,
error,
isSuccess,
status, // "idle" | "pending" | "success" | "error"
execute,
executeFormAction,
setOptimistic,
reset,
} = useServerAction(myAction, {
// Callbacks
onStart: () => console.log("Started"),
onSuccess: ({ data }) => toast.success("Success!"),
onError: ({ err }) => toast.error(err.message),
onFinish: ([data, err]) => console.log("Finished"),
// Initial data
initialData: { count: 0 },
// Retry configuration
retry: {
maxAttempts: 3,
delay: 1000, // or (attempt, err) => attempt * 1000
},
// Persist states while pending
persistErrorWhilePending: false,
persistDataWhilePending: false,
});
Optimistic Updates
"use client";
import { useServerAction } from "zsa-react";
import { toggleLikeAction } from "./actions";
export function LikeButton({ postId, initialLikes }: Props) {
const { execute, data, isOptimistic, setOptimistic } = useServerAction(
toggleLikeAction,
{ initialData: { liked: false, count: initialLikes } }
);
const handleClick = async () => {
// Optimistically update UI
setOptimistic((current) => ({
liked: !current.liked,
count: current.liked ? current.count - 1 : current.count + 1,
}));
// Execute actual action (will rollback on error)
await execute({ postId });
};
return (
<button onClick={handleClick} className={isOptimistic ? "opacity-50" : ""}>
{data.liked ? "â¤ï¸" : "ð¤"} {data.count}
</button>
);
}
Timeouts & Retries
"use server";
import { createServerAction } from "zsa";
import z from "zod";
export const slowAction = createServerAction()
.input(z.object({ data: z.string() }))
.timeout(5000) // 5 second timeout
.retry({
maxAttempts: 3,
delay: (attempt) => attempt * 1000, // Exponential backoff
})
.handler(async ({ input }) => {
// Long-running operation
return processData(input.data);
});
Best Practices
- Create procedures first, reuse across actions – don’t create new procedures per action
- Throw descriptive errors –
throw new Error("Email already exists")for client display - Name destructured results –
const [categories, err] = await getCategoriesAction() - Call Service layer, NOT DAL directly – keep actions thin
- Always validate input with Zod schemas
- Use
revalidatePath/revalidateTagafter mutations - Keep actions thin – business logic belongs in services
File Structure
features/<feature>/usecases/
âââ create/actions/create-<entity>-action.ts
âââ update/actions/update-<entity>-action.ts
âââ delete/actions/delete-<entity>-action.ts
âââ list/actions/list-<entity>-action.ts
Action Pattern
// create-account-action.ts
'use server'
import 'server-only'
import { revalidatePath } from 'next/cache'
import { authedProcedure } from '@saas4dev/auth'
import { CreateAccountSchema, AccountSchema } from '@/features/accounts/model/account-schemas'
import { AccountService } from '@/features/accounts/account-service'
export const createAccountAction = authedProcedure
.createServerAction()
.input(CreateAccountSchema, { type: 'formData' })
.output(AccountSchema)
.onComplete(async () => {
revalidatePath('/accounts')
})
.handler(async ({ input, ctx }) => {
const result = await AccountService.create(ctx.userId, input)
if (!result.success) {
throw new Error(result.error)
}
return result.data
})
Required Directives
Every action file MUST include:
'use server' // First line - marks as server action
import 'server-only' // Prevents client import
React Query Integration
'use client'
import { useServerActionMutation, useServerActionQuery } from '@saas4dev/core'
// Mutations (create, update, delete)
const mutation = useServerActionMutation(createAction, {
onSuccess: () => toast.success('Created'),
onError: (error) => toast.error(error.message),
})
// Usage in forms
const form = useForm<Input>({ resolver: zodResolver(Schema) })
const onSubmit = (data: Input) => mutation.mutate(data)
// Queries (read, list)
const { data, isLoading } = useServerActionQuery(listAction, { input: { userId } })
Reference Files
references/procedures.md: Advanced procedure patterns, chaining, contextreferences/react-query.md: TanStack Query integration with ZSAreferences/forms.md: Form handling, validation, file uploads