Plug-and-play, type-safe server actions with schema validation (schema-agnostic, Zod compatible).
npm install next-action-plusparsedInput type inference
.schema(...).action(async (...) => ...)
bash
npm i next-action-plus
`
If you want Zod schemas, also install:
`bash
npm i zod
`
Quick start
`ts
import { createActionPlus } from 'next-action-plus';
import { z } from 'zod';
export const sayHello = createActionPlus()
.schema(z.object({ name: z.string().min(1) }))
.action(async ({ parsedInput }) => {
return { message: Hello ${parsedInput.name} };
});
`
$3
- sayHello is still “just a function” you can call.
- parsedInput is inferred from your schema.
- The return type is inferred from your handler.
Error handling
If schema validation fails, the action throws an Error.
The error message is intentionally short and looks like:
`txt
Input (name) is error: String must contain at least 1 character(s)
`
Notes:
- Only the first validation issue is used to build the message.
- The thrown error is an ActionPlusValidationError (extends Error) with a developer-friendly payload.
- The original validator error is preserved on error.cause.
- Normalized issues are available on error.issues.
`ts
try {
await sayHello({ name: '' });
} catch (error) {
// error.message => "Input (name) is error: ..."
// (error as ActionPlusValidationError).code => "VALIDATION_ERROR"
// (error as ActionPlusValidationError).issues => [{ path, message, raw }]
// (error as any).cause => original validator error (e.g. ZodError)
}
`
$3
createActionPlus accepts options to control logging and customize thrown errors.
`ts
import { createActionPlus } from 'next-action-plus';
export const client = createActionPlus({
logger: false,
formatValidationError: ({ message, issues, error }) => {
const e = new Error(message);
(e as any).issues = issues;
(e as any).cause = error;
return e;
},
onError: ({ phase, error }) => {
// report errors (Sentry, etc)
// phase: "validation" | "middleware" | "handler"
void error;
},
});
`
Examples
$3
This keeps the native Server Action feel.
`ts
import 'server-only';
import { createActionPlus } from 'next-action-plus';
import { z } from 'zod';
export const updateProfile = createActionPlus()
.schema(z.object({ displayName: z.string().min(2) }))
.action(async ({ parsedInput }) => {
// parsedInput.displayName is string
return { ok: true };
});
`
$3
You can import a Server Action into a Client Component and call it like a normal async function.
Next.js runs it on the server.
`tsx
'use client';
import { useState, useTransition } from 'react';
import { sayHello } from '@/app/actions';
export function SayHelloClient() {
const [name, setName] = useState('');
const [message, setMessage] = useState(null);
const [pending, startTransition] = useTransition();
return (
setName(e.target.value)} placeholder='Ada' />
disabled={pending}
onClick={() =>
startTransition(async () => {
const result = await sayHello({ name });
setMessage(result.message);
})
}
>
{pending ? 'Sending…' : 'Say hello'}
{message ? {message}
: null}
);
}
`
The snippets above are the full examples.
$3
Works with FormData and File using zod-form-data.
If you chain multiple schemas and the input is a FormData, next-action-plus will first try to find a schema that can parse the FormData into a plain object, then validate the remaining schemas against that object.
`ts
import { createActionPlus } from 'next-action-plus';
import { zfd } from 'zod-form-data';
export const uploadAvatar = createActionPlus()
.schema(
zfd.formData({
avatar: zfd.file(),
}),
)
.action(async ({ parsedInput }) => {
// parsedInput.avatar is File
return { filename: parsedInput.avatar.name };
});
`
$3
Add data to context in a type-safe way.
Validation runs first, then middleware runs, then your handler runs.
`ts
import { createActionPlus } from 'next-action-plus';
import { z } from 'zod';
const client = createActionPlus().use(async ({ next }) => next({ ctx: { userId: 'u_123' } }));
export const deletePost = client.schema(z.object({ postId: z.string() })).action(async ({ parsedInput, ctx }) => {
// ctx.userId is string
// parsedInput.postId is string
return { ok: true };
});
`
$3
You are not forced into one validator.
Supported schema shapes:
- Zod (parse / parseAsync)
- Generic { parse(...) } and { parseAsync(...) }
- Standard Schema v1 (~standard.validate)
If you already have a schema system, you can plug it in.
#### Schema chaining
You can chain multiple schemas. If they return objects, outputs are merged.
`ts
import { createActionPlus } from 'next-action-plus';
const s1 = { parse: (_: unknown) => ({ a: 'a' }) };
const s2 = { parse: (_: unknown) => ({ b: 2 }) };
export const demo = createActionPlus()
.schema(s1)
.schema(s2)
.action(async ({ parsedInput }) => {
// parsedInput is { a: string; b: number }
return parsedInput;
});
`
FAQ
$3
No. It is built for the Server Actions style, but it runs in any Node 20+ runtime.
$3
No. The API is intentionally small: createActionPlus() → .schema() → .use() → .action().
$3
Yes. Use zod-form-data (see FormData + File uploads).
$3
No. The returned action function keeps the exact return type of your handler.
$3
Yes. Multiple schemas can validate the same base input. If they produce objects, outputs are merged.
Release
This repository ships with semantic-release.
- Push Conventional Commits to main
- GitHub Actions runs npm test, npm run build`, then publishes