Type-Safe Parsing in Your Server Action
Validation With Zod
Zod schemas validate `FormData` on the server, returning either fully typed data or a map of per-field errors you can render in the form.
What you'll learn
- Define a Zod schema for the form's shape
- Parse `FormData` with `safeParse`
- Return field-level errors to the client
Form data arrives as strings. Zod parses, coerces, and validates it in one step, and its type inference becomes the action’s payload type.
Define the Schema
import { z } from 'zod'
export const signupSchema = z.object({
email: z.string().email('Enter a valid email'),
age: z.coerce.number().int().min(18, 'Must be 18 or older'),
password: z.string().min(8, 'At least 8 characters'),
}) z.coerce.number() is critical: FormData values are always strings, so a plain
z.number() would always fail.
Parse Inside the Action
'use server'
import { signupSchema } from './schema'
type State = { errors?: Record<string, string[]>; ok?: boolean }
export async function signup(prev: State, form: FormData): Promise<State> {
const parsed = signupSchema.safeParse(Object.fromEntries(form))
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors }
}
await db.users.create(parsed.data) // parsed.data is fully typed
return { ok: true }
} safeParse returns { success: false, error } rather than throwing — which is what you
want here, because validation failures are expected, not exceptional.
Render the Errors
'use client'
import { useActionState } from 'react'
import { signup } from './actions'
export function SignupForm() {
const [state, action] = useActionState(signup, {})
return (
<form action={action}>
<input name="email" />
{state.errors?.email?.[0] && <p className="error">{state.errors.email[0]}</p>}
<input name="age" />
{state.errors?.age?.[0] && <p className="error">{state.errors.age[0]}</p>}
<button>Sign up</button>
</form>
)
}