How to Implement Server-Side Validation with Zod in Next.js
I was reviewing different validation libraries the other day when I realized how elegant Zod's TypeScript-first approach is for server-side validation in Next.js. Today, we'll explore how to implement robust server-side validation using Zod and Next.js's Server Actions.
Let's create a simple contact form with email, name, and message fields to demonstrate Zod's powerful validation capabilities.
Here's what we'll build:
// Contact form schema
const contactSchema = z.object({
email: z.string().email("Please enter a valid email"),
name: z.string().min(2, "Name must be at least 2 characters"),
message: z.string().min(10, "Message must be at least 10 characters"),
});
First, let's set up our server action with Zod validation:
// app/actions/contact.ts
import { z } from "zod";
const contactSchema = z.object({
email: z.string().email("Please enter a valid email"),
name: z.string().min(2, "Name must be at least 2 characters"),
message: z.string().min(10, "Message must be at least 10 characters"),
});
export async function submitContact(formData: FormData) {
const validatedFields = contactSchema.safeParse({
email: formData.get("email"),
name: formData.get("name"),
message: formData.get("message"),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
success: false,
};
}
// Here you would typically send the data to your database or email service
const { email, name, message } = validatedFields.data;
try {
// Process the validated data
await saveToDatabase({ email, name, message });
return {
errors: null,
success: true,
};
} catch (error) {
return {
errors: { form: ["Failed to submit form"] },
success: false,
};
}
}
Now, let's create our contact form component that uses this server action:
// app/components/ContactForm.tsx
"use client";
import { useFormState } from "react-dom";
import { submitContact } from "../actions/contact";
const initialState = {
errors: null,
success: false,
};
export default function ContactForm() {
const [state, formAction] = useFormState(submitContact, initialState);
return (
<form action={formAction} className="mx-auto max-w-md space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
type="email"
id="email"
name="email"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
{state.errors?.email && (
<p className="mt-1 text-sm text-red-500">{state.errors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="name" className="block text-sm font-medium">
Name
</label>
<input
type="text"
id="name"
name="name"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
{state.errors?.name && (
<p className="mt-1 text-sm text-red-500">{state.errors.name[0]}</p>
)}
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium">
Message
</label>
<textarea
id="message"
name="message"
rows={4}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
{state.errors?.message && (
<p className="mt-1 text-sm text-red-500">{state.errors.message[0]}</p>
)}
</div>
<button
type="submit"
className="w-full rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
>
Send Message
</button>
{state.success && (
<p className="mt-2 text-sm text-green-500">
Message sent successfully!
</p>
)}
</form>
);
}
What makes Zod particularly powerful is its ability to create complex validation schemas. Let's explore some more advanced validation scenarios:
// Advanced validation examples
const advancedContactSchema = z.object({
email: z
.string()
.email("Please enter a valid email")
.refine((email) => !email.endsWith("@temp.com"), {
message: "Temporary email addresses are not allowed",
}),
name: z
.string()
.min(2, "Name must be at least 2 characters")
.max(50, "Name must be less than 50 characters")
.refine((name) => /^[a-zA-Z\s]*$/.test(name), {
message: "Name can only contain letters and spaces",
}),
message: z
.string()
.min(10, "Message must be at least 10 characters")
.max(1000, "Message must be less than 1000 characters"),
phone: z
.string()
.optional()
.refine(
(phone) => !phone || /^\+?[\d\s-]{10,}$/.test(phone),
"Invalid phone number format"
),
});
Zod's TypeScript integration is where it really shines. You get automatic type inference from your schemas:
// The type is automatically inferred from the schema
type ContactForm = z.infer<typeof contactSchema>;
// TypeScript now knows exactly what your form data should look like
const processContact = (data: ContactForm) => {
// Your data is fully typed!
console.log(data.email); // TypeScript knows this is a string
};
Want to add custom error messages for different validation scenarios? Zod makes it easy:
const customMessageSchema = z.object({
email: z
.string({
required_error: "Email is required",
invalid_type_error: "Email must be a string",
})
.email("Please enter a valid email address")
.min(5, {
message: "Email must be at least 5 characters long",
}),
// ... other fields
});
Zod is incredibly powerful when combined with Next.js Server Actions. The validation happens on the server side, which means:
- Users can't bypass validation by manipulating client-side code
- You get consistent validation across your entire application
- Your validation logic stays close to your data processing logic
The beauty of this setup is that it provides both type safety and runtime validation in a single, elegant package. No more maintaining separate validation schemas and TypeScript interfaces!
Remember to install the required dependencies:
npm install zod
And ensure you have TypeScript configured in your Next.js project:
// tsconfig.json
{
"compilerOptions": {
"strict": true
// ... other options
}
}
Zod is a game-changer for server-side validation in Next.js applications. Its TypeScript-first approach, combined with Next.js Server Actions, provides a robust foundation for handling form submissions and data validation. The schema-based approach means you write your validation rules once and get both runtime validation and TypeScript types for free.
Give it a try in your next project, and you'll wonder how you ever lived without it!