Careeroza
Validation with Zod
Validate on the Server
Browser validation (required, type="email") is for user comfort only. Real validation happens in the action. Zod is the common choice: one schema gives runtime checks and a TypeScript type.
// app/admin/products/actions.ts
'use server';
import { z } from 'zod';
const ProductSchema = z.object({
name: z.string().trim().min(3, 'Name must be at least 3 characters'),
price: z.coerce.number().positive('Price must be more than 0'),
stock: z.coerce.number().int().min(0),
category: z.enum(['mobiles', 'books', 'fashion']),
});
export type FormState = { errors?: Record<string, string[]>; message?: string };
export async function createProduct(prev: FormState, formData: FormData): Promise<FormState> {
const parsed = ProductSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { errors: parsed.error.flatten().fieldErrors };
// parsed.data is { name: string; price: number; stock: number; category: 'mobiles' | 'books' | 'fashion' }
await saveProduct(parsed.data);
return { message: 'Product created' };
}How It Works Inside
Example
A tiny Zod-like validator for form fields (strings in, typed values or field errors out):
type Check<T> = (raw: string) => { ok: true; value: T } | { ok: false; error: string };
const text = (min: number): Check<string> => (raw) =>
raw.trim().length >= min ? { ok: true, value: raw.trim() } : { ok: false, error: 'at least ' + min + ' characters' };
const positive: Check<number> = (raw) => {
const n = Number(raw);
return raw !== '' && n > 0 ? { ok: true, value: n } : { ok: false, error: 'must be more than 0' };
};
function validate<S extends Record<string, Check<unknown>>>(schema: S, input: Record<string, string>) {
const data: Record<string, unknown> = {};
const errors: Record<string, string> = {};
for (const key of Object.keys(schema)) {
const r = schema[key](input[key] ?? '');
if (r.ok) data[key] = r.value; else errors[key] = r.error;
}
return Object.keys(errors).length
? { success: false as const, errors }
: { success: true as const, data: data as { [K in keyof S]: S[K] extends Check<infer T> ? T : never } };
}
const schema = { name: text(3), price: positive };
console.log(validate(schema, { name: 'Clean Code', price: '450' }));
console.log(validate(schema, { name: ' TV', price: '-5' }));Output
{ success: true, data: { name: 'Clean Code', price: 450 } }
{
success: false,
errors: { name: 'at least 3 characters', price: 'must be more than 0' }
}Showing Field Errors
'use client';
import { useActionState } from 'react';
import { createProduct } from './actions';
export default function ProductForm() {
const [state, action, pending] = useActionState(createProduct, {});
return (
<form action={action}>
<input name="name" />
{state.errors?.name && <p className="text-red-600">{state.errors.name[0]}</p>}
<input name="price" type="number" />
{state.errors?.price && <p className="text-red-600">{state.errors.price[0]}</p>}
<button disabled={pending}>Save</button>
</form>
);
}