Next.js 16 Form Validation with Zod (Server-Side Example)
NextJS

Next.js 16 Form Validation with Zod (Server-Side Example)

Next.js 16 Edit Form Tutorial: Server-Side Validation with Zod

Author
Richard Mendes
April 04, 2026 • 5 mins

This page contains the complete setup for editing a transaction in Next.js 16, including the page, client form, and server-side Zod validation.

You can copy and use this code directly in your project.

Edit Transaction Page

import Link from "next/link";
import prisma from "@/lib/prisma";
import EditTransactionForm from "./EditTransactionForm";

type PageProps = {
params: {
id: string;
};
};

export default async function EditTransactionPage({ params }: PageProps) {
const { id } = await params;

const transaction = await prisma.transaction.findUnique({
where: { id: BigInt(id) },
});

if (!transaction) {
return (
<div>
<p>Transaction not found.</p>
<Link href="/transactions">Back to Transactions</Link>
</div>
);
}

return <EditTransactionForm transaction={transaction} />;
}

EditTransactionForm (Client Component)

"use client";
import Link from "next/link";
import { useActionState } from "react";
import { updateTransaction } from "@/app/actions/updateTransaction";
import { useEffect } from "react";

type Props = {
transaction: {
id: bigint;
amount: number | null;
type: string | null;
description: string | null;
};
};

type State = {
errors?: {
amount?: string[];
type?: string[];
description?: string[];
};
};

export default function EditTransactionForm({ transaction }: Props) {
const initialState: State = { errors: {} };
const [state, formAction] = useActionState(updateTransaction, initialState);

useEffect(() => {
if (state?.errors) {
console.log("Validation errors:", state.errors);
}
}, [state]);

return (
<form action={formAction}>
<input type="hidden" name="id" value={transaction.id.toString()} />

<input type="number" name="amount" defaultValue={transaction.amount ?? ""} />
<select name="type" defaultValue={transaction.type ?? ""}>
<option value="income">Income</option>
<option value="expense">Expense</option>
</select>
<input type="text" name="description" defaultValue={transaction.description ?? ""} />

<button type="submit">Update Transaction</button>
<Link href="/transactions">Cancel</Link>
</form>
);
}

Server Action with Zod Validation

"use server";
import prisma from "@/lib/prisma";
import { redirect } from "next/navigation";
import * as z from "zod";

const formSchema = z.object({
amount: z.number().positive(),
type: z.enum(["income", "expense"]),
description: z.string().min(5).max(255),
});

export async function updateTransaction(prevState: any, formData: FormData) {
const rawData = {
id: BigInt(formData.get("id") as string),
amount: parseFloat(formData.get("amount") as string),
type: formData.get("type") as string,
description: formData.get("description") as string,
};

const validatedResult = formSchema.safeParse(rawData);

if (!validatedResult.success) {
return { errors: validatedResult.error.flatten().fieldErrors };
}

await prisma.transaction.update({
where: { id: rawData.id },
data: validatedResult.data,
});

redirect("/transactions");
}



Popular Posts

Comments (0)

No comments yet. Be the first to comment!

Latest Articles