Every form submission needed: an API route, a fetch call, error handling on both sides, type definitions shared between client and server. For a contact form, that's 4 files minimum.
Server Actions collapse this into a single function.
// app/contact/actions.ts
"use server";
import { z } from "zod";
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
});
export async function submitContact(formData: FormData) {
const parsed = schema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
});
if (!parsed.success) {
return { error: "Invalid input" };
}
await db.contactSubmission.create({ data: parsed.data });
return { success: true };
}The form component calls it directly, no fetch, no API URL, no CORS:
"use client";
import { submitContact } from "./actions";
function ContactForm() {
const [state, formAction] = useActionState(submitContact, null);
return <form action={formAction}>...</form>;
}revalidatePath() or revalidateTag() after mutationsIn our Excel Flow dashboard, switching from API routes to Server Actions for all CRUD operations cut 1,200 lines of code and eliminated an entire category of bugs (mismatched request/response types).
Server Actions aren't a toy feature. They're a fundamental shift in how we build full-stack React apps.