next-safe-action
API reference

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

On this page