计划执行与检查节点
executing-plans
按既定实施计划完成工作,保持过程检查和反馈。
next-safe-action【效率与协作】next-safe-action is a library for type-safe Next.js Server Actions with schema validation, middleware and typed errors. Use it when a user asks to validate server action inputs with Zod, handle errors in server actions, add authentication middleware to actions, call actions from client components with hooks, or build type-safe mutations in the Next.j…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
next-safe-action wraps Next.js Server Actions with a builder: you attach an input schema, optional middleware and metadata, then write the server code. Callers get a typed result object instead of exceptions: data on success, validationErrors when input is invalid, serverError when your code throws.
Version checked: 8.7.3 (peer deps: next >= 14, react >= 18.2). Differences from older (v6/v7) tutorials:
.inputSchema(schema); .schema() still exists but is deprecated..use(async ({ next }) => next({ ctx })). The middleware option of createSafeActionClient no longer exists.execute takes the input object, not FormData. Use useStateAction's formAction, or build the object in your own form handler.next-safe-action/hooks (useAction, useOptimisticAction) and next-safe-action/stateful-hooks (useStateAction).npm install next-safe-action zod
// lib/safe-action.ts
import { createSafeActionClient, DEFAULT_SERVER_ERROR_MESSAGE } from 'next-safe-action'
import { auth } from '@/auth'
class AppError extends Error {}
export const actionClient = createSafeActionClient({
// Whatever you return becomes result.serverError; never leak raw error messages.
handleServerError(e) {
if (e instanceof AppError) return e.message
console.error('Action failed:', e)
return DEFAULT_SERVER_ERROR_MESSAGE
},
})
export const authActionClient = actionClient.use(async ({ next }) => {
const session = await auth()
if (!session?.user) throw new AppError('You must be signed in.')
return next({ ctx: { user: session.user } })
})
auth() stands for your auth library (Auth.js, Clerk, Better Auth). Middleware added with .use() runs before input validation; for logic that needs the parsed input use .useValidated() after .inputSchema().
// actions/projects.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { returnValidationErrors } from 'next-safe-action'
import { authActionClient } from '@/lib/safe-action'
import { prisma } from '@/lib/db'
const createProjectSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().max(500).optional(),
})
export const createProject = authActionClient
.inputSchema(createProjectSchema)
.action(async ({ parsedInput, ctx }) => {
const taken = await prisma.project.findFirst({
where: { ownerId: ctx.user.id, name: parsedInput.name },
})
if (taken) {
returnValidationErrors(createProjectSchema, {
name: { _errors: ['You already have a project with this name'] },
})
}
const project = await prisma.project.create({
data: { ...parsedInput, ownerId: ctx.user.id },
})
revalidatePath('/dashboard')
return { project }
})
Always check ownership inside the action (where: { id, ownerId: ctx.user.id }); authentication middleware alone does not authorize access to a specific row.
// components/CreateProjectForm.tsx
'use client'
import { useAction } from 'next-safe-action/hooks'
import { createProject } from '@/actions/projects'
export function CreateProjectForm() {
const { execute, result, isExecuting, hasSucceeded } = useAction(createProject)
return (
<form
action={(formData: FormData) =>
execute({
name: String(formData.get('name') ?? ''),
description: String(formData.get('description') ?? '') || undefined,
})
}
>
<input name="name" placeholder="Project name" required />
{result.validationErrors?.name?._errors?.map((m) => <p key={m}>{m}</p>)}
<textarea name="description" placeholder="Description (optional)" />
{result.serverError && <p role="alert">{result.serverError}</p>}
{hasSucceeded && <p>Created {result.data?.project.name}</p>}
<button disabled={isExecuting}>{isExecuting ? 'Creating…' : 'Create project'}</button>
</form>
)
}
validationErrors is shaped like the schema: { _errors?: string[], name?: { _errors: string[] } }. The package also exports flattenValidationErrors and formatValidationErrors for reshaping errors. useAction also accepts onSuccess, onError and onSettled callbacks, and executeAsync returns the result as a promise.
User request: "Add an archive button to the project list that updates instantly and fails safely."
Define archiveProject with authActionClient.inputSchema(z.object({ id: z.string().uuid() })), update with where: { id, ownerId: ctx.user.id } and return { id }. In the component, const { execute, optimisticState } = useOptimisticAction(archiveProject, { currentState: projects, updateFn: (state, { id }) => state.filter((p) => p.id !== id) }). Clicking the button hides the row immediately; if the action returns serverError the list snaps back.
User request: "My action throws 'Not found' but the client only shows a generic message."
That is the default: next-safe-action hides thrown errors behind DEFAULT_SERVER_ERROR_MESSAGE. Throw a custom error class and return its message from handleServerError as in Step 1, log everything else on the server, and read result.serverError in the component. Do not return e.message for every error: database and library messages can reveal internals.
'use server' files exporting only actions; client components import the exported action and pass it to a hook.revalidatePath or revalidateTag after mutations so cached pages update.redirect() from next/navigation inside actions only when you want navigation; the hooks report it as hasNavigated.