Server-Side Form Validation with Zod in Next.js

Validate form fields inside a Server Action with Zod: define a schema, safeParse the fields, and return field errors to the form.

6 min read

Zod form validation in Next.js happens inside the Server Action, where the rules cannot be bypassed from the browser. Zod gives you a schema for that check: define the shape and constraints once, then parse the form fields against it.

Install Zod first.

bashbash
npm install zod

Define a schema for the fields. Each key matches a form field name, and each constraint maps to a rule the value must satisfy.

typescripttypescript
// app/schema.ts
import { z } from 'zod'
 
export const signupSchema = z.object({
  name: z.string().trim().min(2),
  email: z.email(),
})

The schema marks name as a trimmed string of at least two characters and email as a valid address. It says nothing about your form yet; it is only a description of the data you expect.

These examples use Zod 4, where each string format is a top-level function. On Zod 3 the same rule is written as z.string().email(), which still works in Zod 4 but is deprecated.

Now parse the submitted fields against it.

typescripttypescript
// app/actions.ts
'use server'
import { z } from 'zod'
import { signupSchema } from './schema'
 
export async function signUp(formData: FormData) {
  const fields = { name: formData.get('name'), email: formData.get('email') }
  const result = signupSchema.safeParse(fields)
  if (!result.success) return { errors: z.flattenError(result.error).fieldErrors }
  return { ok: true }
}

The action builds an object from the form fields and runs safeParse on it. When the fields pass, result.success is true and you proceed to the write. When they fail, the action returns the field errors instead.

Read the field errors

z.flattenError turns the error into a shallow object with two keys. formErrors holds messages that belong to the whole object, and fieldErrors maps each field name to an array of message strings.

Submitting the name "A" with the email "nope" returns this:

texttext
{
  formErrors: [],
  fieldErrors: {
    name: [ 'Too small: expected string to have >=2 characters' ],
    email: [ 'Invalid email address' ]
  }
}

Each value is an array because one field can break several rules at once. Rendering the first entry is usually enough, and the rest stay available if you want to list them.

That shape is why Zod pairs so well with Server Actions. The action returns one object, and the form looks up the message for each input by field name without any extra parsing.

Client validation is not a guard

An HTML required attribute catches empty fields before the request leaves the browser, which saves the visitor a round trip. It does not protect the action, because the action is reachable over HTTP and can be called directly without the form.

CheckWhere it runsCan a visitor skip it
required attributeBrowserYes
Zod schemaServer actionNo

A direct POST can bypass the browser entirely, so the schema is the only check that always runs. Keep the required attribute for convenience and the Zod schema for authority.

See displaying field-level validation errors from a Server Action to render those messages under each input, and reading FormData in a Server Action for the field methods.

Parse, then write

After the schema passes, use the parsed data from result.data, not the raw form fields. The schema applies its own transforms, so a trimmed name reaches the database while the original FormData stays untouched.

The types line up too. Inside the success branch, result.data is typed from the schema, so the write step gets strings rather than the string, File, or null union that formData.get returns.

A schema still only checks the shape of the input. A well formed payload can name a record the visitor does not own, so authenticate and authorize inside the action as well before the write runs.

Rune AI

Rune AI

Key Insights

  • Install Zod and define a schema that mirrors the form fields.
  • Pass the submitted fields to safeParse.
  • Check result.success before writing anything.
  • Return z.flattenError(result.error).fieldErrors on failure.
  • Treat HTML attributes as convenience, never as the real guard.
RunePowered by Rune AI

Frequently Asked Questions

Why validate on the server at all?

The Server Action is reachable over HTTP, so a script can submit data without going through your form. Server-side validation is the only check that always runs.

What does safeParse return?

An object with a success flag. On success it has a data property with the parsed values. On failure it has an error property you pass to z.flattenError to get per-field messages.

Should I keep the HTML required attribute?

Yes, as a convenience for visitors. It saves a round trip but does not protect the action, so always keep the Zod schema as the authoritative check.

Conclusion

Validate form fields inside the Server Action with a Zod schema. Run safeParse on the submitted values, and on failure return the fieldErrors from z.flattenError so the form can show a message under each field.