Search for a command to run...
Last updated June 9, 2026
ctrovalidate-next provides validateAction and formDataToValues for server-side validation in Next.js Server Actions. Combined with ctrovalidate-react on the client, you get a full-stack validation story with shared schemas.
npm install ctrovalidate-next ctrovalidate-coreRequirements: Next.js >=13.4.0
Define your schema once in a shared file to ensure identical logic on both client and server:
// lib/schemas.ts
export const signupSchema = {
email: 'required|email',
password: 'required|minLength:8',
};validateAction'use server';
import { validateAction } from 'ctrovalidate-next';
import { signupSchema } from '@/lib/schemas';
export async function signup(prevState: unknown, formData: FormData) {
const { isValid, errors, values } = await validateAction<{
email: string;
password: string;
}>(formData, signupSchema);
if (!isValid) {
return { success: false, errors };
}
// values is typed — use it safely
await db.user.create({ data: values });
return { success: true };
}| Param | Type | Description |
|---|---|---|
formData | FormData | Native FormData from the Server Action |
schema | ValidationSchema | Field-to-rules mapping |
options | ValidateActionOptions | Optional (customRules, aliases, messages, locale) |
ValidateActionOptionsinterface ValidateActionOptions {
customRules?: Record<string, RuleLogic | AsyncRuleLogic>;
aliases?: Record<string, SchemaRule>;
messages?: Record<string, string>;
locale?: string;
}Promise<{
isValid: boolean;
errors: Partial<Record<keyof T, string>>;
values: T;
}>isValid — true only if all schema fields passerrors — Object with error strings for failed fields; valid fields are omittedvalues — FormData parsed into a typed object (see formDataToValues)useCtrovalidate (Optional)For instant feedback before submission, use ctrovalidate-react in 'use client' components with the same schema:
'use client';
import { useCtrovalidate } from 'ctrovalidate-react';
import { signupSchema } from '@/lib/schemas';
import { signup } from './actions';
import { useActionState } from 'react';
export default function SignupForm() {
const { values, errors, handleChange, handleBlur } =
useCtrovalidate({
initialValues: { email: '', password: '' },
schema: signupSchema,
});
const [state, action, isPending] = useActionState(signup, {
success: false,
errors: {},
});
return (
<form action={action}>
<input
name="email"
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
/>
{errors.email && <p>{errors.email}</p>}
{state.errors?.email && <p>{state.errors.email}</p>}
<input
type="password"
name="password"
value={values.password}
onChange={(e) => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
/>
{errors.password && <p>{errors.password}</p>}
{state.errors?.password && <p>{state.errors.password}</p>}
<button type="submit" disabled={isPending}>Sign Up</button>
</form>
);
}formDataToValues<T>(formData)Converts FormData to a typed plain object. Handles multiple values for the same key (checkboxes, multi-select) by collecting them into an array.
import { formDataToValues } from 'ctrovalidate-next';
interface Preferences {
interests: string[];
newsletter: string;
}
const values = formDataToValues<Preferences>(formData);
// If "interests" checkbox appears 3 times → values.interests is ['a', 'b', 'c']Behavior:
string[]validateAction. Client-side validation is UX-only.lib/schemas.ts to keep client and server in sync.useActionState/useFormState) after submission.ctrovalidate-core and ctrovalidate-next work in the Edge Runtime.