Getting started
- Next.js 15.1
- React >= 19
- TypeScript >= 5
- A supported validation library: Zod, Valibot, Yup, TypeBox
- Next.js >= 14 (>= 15 for
useStateAction
hook) - React >= 18.2.0
- TypeScript >= 5
- A supported validation library: Zod, Valibot, Yup, TypeBox
Next.js >= 15.1 and React 19 are required for using next-safe-action >= 7.10.0. This is due to internal error handling framework changes. So, please upgrade to the latest version to use this library with Next.js 15.0.5 or later.
next-safe-action provides a typesafe Server Actions implementation for Next.js App Router applications.
Installation
The library works with multiple validation libraries, at this time:
- Zod
- Valibot
- Yup
- TypeBox
Choose your preferred one in the tabs below to get the correct instructions.
- Zod
- Valibot
- Yup
- TypeBox
- npm
- Yarn
- pnpm
npm i next-safe-action zod
yarn add next-safe-action zod
pnpm add next-safe-action zod
- npm
- Yarn
- pnpm
npm i next-safe-action valibot
yarn add next-safe-action valibot
pnpm add next-safe-action valibot
- npm
- Yarn
- pnpm
npm i next-safe-action yup
yarn add next-safe-action yup
pnpm add next-safe-action yup
- npm
- Yarn
- pnpm
npm i next-safe-action @sinclair/typebox
yarn add next-safe-action @sinclair/typebox
pnpm add next-safe-action @sinclair/typebox
Usage
1. Instantiate a new client
You can create a new client with the following code:
- Zod
- Valibot
- Yup
- TypeBox
import { createSafeActionClient } from "next-safe-action";
export const actionClient = createSafeActionClient();
When using Zod, you don't need to specify a validationAdapter
, because it's the default validation library for next-safe-action.
import { createSafeActionClient } from "next-safe-action";
import { valibotAdapter } from "next-safe-action/adapters/valibot";
export const actionClient = createSafeActionClient({
validationAdapter: valibotAdapter(),
});
import { createSafeActionClient } from "next-safe-action";
import { yupAdapter } from "next-safe-action/adapters/yup";
export const actionClient = createSafeActionClient({
validationAdapter: yupAdapter(),
});
import { createSafeActionClient } from "next-safe-action";
import { typeboxAdapter } from "next-safe-action/adapters/typebox";
export const actionClient = createSafeActionClient({
validationAdapter: typeboxAdapter(),
});
This is a basic client, without any options or middleware functions. If you want to explore the full set of options, check out the create the client section.
2. Define a new action
This is how a safe action is created. Providing a validation input schema to the function via schema()
, we're sure that data that comes in is type safe and validated.
The action()
method lets you define what happens on the server when the action is called from client, via an async function that receives the parsed input and context as arguments. In short, this is your server code. It never runs on the client:
- Zod
- Valibot
- Yup
- TypeBox
"use server"; // don't forget to add this!
import { z } from "zod";
import { actionClient } from "@/lib/safe-action";
// This schema is used to validate input from client.
const schema = z.object({
username: z.string().min(3).max(10),
password: z.string().min(8).max(100),
});
export const loginUser = actionClient
.schema(schema)
.action(async ({ parsedInput: { username, password } }) => {
if (username === "johndoe" && password === "123456") {
return {
success: "Successfully logged in",
};
}
return { failure: "Incorrect credentials" };
});
"use server"; // don't forget to add this!
import * as v from "valibot";
import { actionClient } from "@/lib/safe-action";
// This schema is used to validate input from client.
const schema = v.object({
username: v.pipe(v.string(), v.minLength(3), v.maxLength(10)),
password: v.pipe(v.string(), v.minLength(8), v.maxLength(100)),
});
export const loginUser = actionClient
.schema(schema)
.action(async ({ parsedInput: { username, password } }) => {
if (username === "johndoe" && password === "123456") {
return {
success: "Successfully logged in",
};
}
return { failure: "Incorrect credentials" };
});
"use server"; // don't forget to add this!
import * as y from "yup";
import { actionClient } from "@/lib/safe-action";
// This schema is used to validate input from client.
const schema = y.object({
username: y.string().min(3).max(10).required(),
password: y.string().min(8).max(100).required(),
});
export const loginUser = actionClient
.schema(schema)
.action(async ({ parsedInput: { username, password } }) => {
if (username === "johndoe" && password === "123456") {
return {
success: "Successfully logged in",
};
}
return { failure: "Incorrect credentials" };
});
"use server"; // don't forget to add this!
import { Type } from "@sinclair/typebox";
import { actionClient } from "@/lib/safe-action";
// This schema is used to validate input from client.
const schema = Type.Object({
username: Type.String({ minLength: 3, maxLength: 10 }),
password: Type.String({ minLength: 8, maxLength: 100 }),
});
export const loginUser = actionClient
.schema(schema)
.action(async ({ parsedInput: { username, password } }) => {
if (username === "johndoe" && password === "123456") {
return {
success: "Successfully logged in",
};
}
return { failure: "Incorrect credentials" };
});
action
returns a function that can be called from the client.
3. Import and execute the action
In this example, we're directly calling the Server Action from a Client Component:
"use client"; // this is a Client Component
import { loginUser } from "./login-action";
export default function Login() {
return (
<button
onClick={async () => {
// Typesafe action called from client.
const res = await loginUser({
username: "johndoe",
password: "123456",
});
// Result keys.
res?.data;
res?.validationErrors;
res?.bindArgsValidationErrors;
res?.serverError;
}}>
Log in
</button>
);
}
You also can execute Server Actions with hooks, which are a more powerful way to handle mutations. For more information about these, check out the useAction
, useOptimisticAction
and useStateAction
hooks sections.