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.
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.