mastering zod validation in next.js server actions - By Sourav Mishra (@souravvmishra)

let's see how to build type-safe forms in next.js using zod and server actions. super simple and clean.

BySourav Mishra2 min read

today i want to show you how to build rock-solid forms in next.js using zod and server actions.

if you don't validate your inputs, you're just asking for broken apps and security issues.


why use zod?

zod is basically the go-to schema tool for typescript now. it gives you runtime validation and infers your types automatically.

plus, combining it with next.js server actions means invalid data never even processes on your server.


1. setting up the schema

first, define your schema in a shared file.

// lib/schemas.ts
import { z } from "zod";

export const contactSchema = z.object({
  email: z.string().email(),
  message: z.string().min(10),
  type: z.enum(["support", "feedback", "general"]),
});

2. writing the server action

we use safeParse in the server action. it doesn't throw errors and crash things, it just returns a success or error object.

// app/actions.ts
"use server";
import { contactSchema } from "@/lib/schemas";

export async function submitContactForm(prevState: any, formData: FormData) {
  const data = {
    email: formData.get("email"),
    message: formData.get("message"),
    type: formData.get("type"),
  };

  const parsed = contactSchema.safeParse(data);

  if (!parsed.success) {
    return {
      success: false,
      errors: parsed.error.flatten().fieldErrors,
    };
  }

  // save to db here...
  return { success: true, message: "sent!" };
}

3. the client form

use useActionState to handle the form lifecycle and show errors.

// components/ContactForm.tsx
"use client";

import { useActionState } from "react";
import { submitContactForm } from "@/app/actions";

export function ContactForm() {
  const [state, action, isPending] = useActionState(submitContactForm, {});

  return (
    <form action={action} className="space-y-4">
      {state.success && <div className="text-green-500">{state.message}</div>}

      <input name="email" className="border p-2" placeholder="email" />
      {state.errors?.email && <p className="text-red-500">{state.errors.email[0]}</p>}

      <textarea name="message" className="border p-2" placeholder="message" />
      {state.errors?.message && <p className="text-red-500">{state.errors.message[0]}</p>}

      <input type="hidden" name="type" value="general" />

      <button type="submit" disabled={isPending} className="bg-black text-white p-2">
        {isPending ? "sending..." : "send"}
      </button>
    </form>
  );
}

what about advanced validation?

sometimes you need to check if passwords match. zod's refine is perfect for this.

const passwordSchema = z.object({
  password: z.string(),
  confirm: z.string(),
}).refine((data) => data.password === data.confirm, {
  message: "passwords don't match",
  path: ["confirm"], 
});

zod and next.js server actions are a match made in heaven. it just works beautifully.

for more typescript stuff, read my typescript generics guide.


want a better ui workflow? check out why i use shadcn.

Share this post

Cover image for mastering zod validation in next.js server actions

You might also like

See all