Type-safe server action handling for Next.js — effortless server action validation with Zod, built-in middleware, and great developer ergonomics.
next-server-actions
is a lightweight utility designed to make working with Next.js Server Actions easier and more enjoyable. It provides a clean API for managing form submissions, validation, middleware, and error handling — all with minimal boilerplate.
- ✅ Server Action Integration – Built specifically for Server Actions in Next.js
⚠️ Field & Form-Level Errors – Handle validation like a pro- 🔄 Loading State Management – Easily show loading indicators during submission
- 🔐 Middleware Support – Add authentication, authorization, or custom checks per action
- 🔁 DRY-Friendly – Avoid repeating boilerplate in your server logic
npm install next-server-actions
This creates a createServerAction()
function that can be reused for all your form actions
import { createClient } from "next-server-actions";
export const createServerAction = createClient({
// Optional: add middleware here (e.g. auth, logging, etc.)
});
This validates the form on the server using the schema before executing any logic.
"use server";
import { createServerAction } from "../utils/server-actions";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export const signIn = createServerAction(schema, async (values) => {
// Your login logic here
return { ok: true };
});
This uses useActionState
to bind your form to the server action.
"use client";
import Form from "next/form";
import { useActionState } from "react";
import { signIn } from "../_lib/actions/sign-in";
export function SignInForm() {
const [state, action, pending] = useActionState(signIn, {
ok: false,
});
return (
<Form action={action}>
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" required />
<br />
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" required />
<hr />
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
</Form>
);
}
Middleware allows you to intercept and validate requests before they reach your server action logic. This is useful for enforcing authentication, role-based access control, or other custom request checks.
You can define middleware by using the middleware
option when creating your server action client. This function runs before your server action executes and can return a response to short-circuit the action—for example, if a user is not authenticated.
import { createClient } from "next-server-actions";
export const createServerAction = createClient({
middleware: async () => {
// replace with your auth logic
const isAuthenticated = false;
if (!isAuthenticated) {
return { message: "unauthorized" };
}
},
});
If the middleware returns an object, the corresponding action will not run. Instead, that object will be returned as the response to the client.
The returned message
(or other fields) can be accessed via the state
from useActionState
—just like a regular action response.
"use client";
import Form from "next/form";
import { useActionState } from "react";
import { signIn } from "../_lib/actions/sign-in";
export function SignInForm() {
const [state, action, pending] = useActionState(signIn, {
ok: false,
});
return (
<Form action={action}>
{/* Form-level error from middleware */}
{!state.ok && state.message && <p>{state.message}</p>}
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" />
<br />
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" />
<hr />
<button type="submit">Submit</button>
</Form>
);
}
The context
feature allows you to inject shared data into all your server actions.
For example, instead of calling getUser()
in every individual action, you can call it once in the context
function. The returned object will be automatically passed as the second argument to all server actions.
This helps reduce duplication and keeps your action logic clean and focused.
You can define a shared context using the context
option when creating your server action client. The context
function must return an object, which can contain any data your server actions might need.
import { createClient } from "next-server-actions";
import { getUser } from "../../_data/get-user";
export const createServerAction = createClient({
context: async () => {
const user = await getUser();
return { user };
}
});
The context object you return becomes the second argument in all server action handlers.
"use server";
import { createServerAction } from "../utils/server-actions";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export const signIn = createServerAction<typeof schema>(
schema,
async (values, { user }) => {
// Your login logic using the context-provided user
return { ok: true };
},
);
With this setup, you only need to define logic like getUser()
once, making your actions more reusable and maintainable.
Easily integrate server actions with your UI using shadcn/ui components.