SafeActionClient
The SafeActionClient class is the core of next-safe-action. It provides a chainable, immutable API for building type-safe server actions. Each method returns a new client instance, the original is never modified.
import { createSafeActionClient } from "next-safe-action";
const actionClient = createSafeActionClient();.use()
Add a middleware function to the action execution chain. Cannot be called after useValidated().
client.use(middlewareFn)Parameters:
Prop
Type
The middleware function receives:
Prop
Type
Returns: A new SafeActionClient with NextCtx merged into the context type.
const authClient = actionClient.use(async ({ next }) => {
const session = await getSession();
if (!session) throw new Error("Unauthorized");
return next({ ctx: { user: session.user } });
});
// authClient now has ctx: { user: User }.useValidated()
Add a validated middleware function that runs after input validation. Only available after inputSchema() or bindArgsSchemas() has been called. Cannot be followed by inputSchema(), bindArgsSchemas(), or use().
For most middleware, prefer .use() instead. .useValidated() is designed for the specific cases where your middleware logic depends on the validated parsedInput, such as resource ownership checks or input-dependent authorization.
client.useValidated(middlewareFn)Parameters:
Prop
Type
The validated middleware function receives:
Prop
Type
Returns: A new SafeActionClient with NextCtx merged into the context type. After calling useValidated(), inputSchema(), bindArgsSchemas(), and use() are no longer callable (TypeScript error).
const protectedAction = authClient
.inputSchema(z.object({ postId: z.string() }))
.useValidated(async ({ parsedInput, ctx, next }) => {
const post = await db.post.findUnique({ where: { id: parsedInput.postId } });
if (post?.authorId !== ctx.user.id) throw new Error("Forbidden");
return next({ ctx: { post } });
});
// protectedAction now has ctx: { user: User, post: Post }useValidated() middleware is completely skipped if input validation fails. It only runs when all schemas pass.
.metadata()
Set metadata for the action. Only available when a metadata schema has been defined via defineMetadataSchema in createSafeActionClient.
client.metadata(data)Parameters:
Prop
Type
Returns: A new SafeActionClient with metadata provided.
const myAction = actionClient
.metadata({ actionName: "createUser" })
.action(async ({ parsedInput, metadata }) => {
console.log(metadata.actionName); // "createUser"
});When a metadata schema is defined, you must call .metadata() before .action() or .stateAction(). TypeScript will error if you forget.
.inputSchema()
Define the input validation schema. Accepts a Standard Schema validator (Zod, Valibot, ArkType, etc.) or an async factory function that returns one.
client.inputSchema(schema, utils?)Parameters:
Prop
Type
Returns: A new SafeActionClient with typed parsedInput.
// Direct schema
const action = actionClient
.inputSchema(z.object({ name: z.string() }))
.action(async ({ parsedInput }) => {
// parsedInput: { name: string }
});
// Async factory (for i18n or extending previous schemas)
const action = actionClient
.inputSchema(async () => {
const t = await getTranslations();
return z.object({ name: z.string().min(2, t("name.tooShort")) });
})
.action(async ({ parsedInput }) => { /* ... */ });.schema() is a deprecated alias for .inputSchema(). Use .inputSchema() instead.
.inputSchema() cannot be called after .useValidated(). TypeScript will report an error if you try.
.outputSchema()
Define the output data validation schema. The action's return value is validated against this schema.
client.outputSchema(schema)Parameters:
Prop
Type
Returns: A new SafeActionClient with typed and validated output data.
const action = actionClient
.outputSchema(z.object({ id: z.string(), created: z.boolean() }))
.action(async () => {
return { id: "123", created: true };
// TypeScript + runtime validation ensures this shape
});.bindArgsSchemas()
Define validation schemas for bind arguments. Bind args are additional arguments bound to the action function before the main input.
client.bindArgsSchemas(schemas)Parameters:
Prop
Type
Returns: A new SafeActionClient with typed bind argument inputs.
const action = actionClient
.inputSchema(z.object({ title: z.string() }))
.bindArgsSchemas([z.string().uuid()]) // bind arg: projectId
.action(async ({ parsedInput, bindArgsParsedInputs: [projectId] }) => {
// parsedInput: { title: string }
// projectId: string
});
// In a component:
const boundAction = action.bind(null, projectId);.bindArgsSchemas() cannot be called after .useValidated(). TypeScript will report an error if you try.
.action()
Define the server-side code for the action. This terminates the builder chain and returns the callable action function.
client.action(serverCodeFn, utils?)Parameters:
Prop
Type
The serverCodeFn receives a single argument object:
Prop
Type
Returns: A SafeActionFn, a callable async function.
.stateAction()
Define a stateful server action for use with the useStateAction hook or React's useActionState.
client.stateAction(serverCodeFn, utils?)Same as .action(), but the serverCodeFn receives a second argument utils with access to prevResult:
Prop
Type
Returns: A SafeStateActionFn, a callable async function compatible with useActionState.
ActionCallbacks (action callbacks)
The optional second argument to .action() and .stateAction():
Prop
Type
In onError, onSettled, and onNavigation callbacks, context added by useValidated() middleware is optional in the ctx type. Context from use() middleware is always present, but useValidated() context may not exist if validation failed before validated middleware could run.
export const myAction = actionClient
.inputSchema(schema)
.action(
async ({ parsedInput }) => {
return await doSomething(parsedInput);
},
{
onSuccess: async ({ data, metadata }) => {
console.log("Success:", data);
},
onError: async ({ error }) => {
await reportError(error.serverError);
},
}
);Method chaining order
Methods can be called in any order with a few constraints: use() must come before useValidated(), schemas must come before useValidated(), and the final call must be .action() / .stateAction().
Typical order:
actionClient
.use(middleware) // 1. Add pre-validation middleware (repeatable)
.metadata(data) // 2. Set metadata (if schema defined)
.inputSchema(schema) // 3. Define input validation
.bindArgsSchemas([...]) // 4. Define bind args (optional, must be before useValidated)
.useValidated(middleware) // 5. Add post-validation middleware (repeatable, requires schema)
.outputSchema(schema) // 6. Define output validation (optional)
.action(fn, utils) // 7. Define server code (terminal)See also
- Action client: conceptual overview and immutability pattern
- Middleware guide: practical patterns for
.use()and.useValidated() createSafeActionClient(): creating the client instancecreateMiddleware()/createValidatedMiddleware(): standalone middleware factories- Types reference: all exported TypeScript types