Build and Validate Forms with App Router’s New Action-Based Architecture
Handling forms in Next.js 14 has evolved. Instead of relying entirely on API routes or client state, you can now use Server Actions — a cleaner, declarative way to submit forms directly to the server. In this module, you’ll learn to build modern forms, validate them with Zod, and connect them to real backends.
✅ What Are Server Actions?
Server Actions let you run server code (like DB writes) directly from forms, without setting up separate API routes.
Syntax:
<form action={serverFunction}>
<input name="email" />
<button type="submit">Submit</button>
</form>
Your function must be async and explicitly marked:
'use server';
export async function serverFunction(formData: FormData) {
const email = formData.get('email');
// do something
}
🧪 Example: Simple Contact Form with Server Action
🔹 src/app/contact/page.tsx
import ContactForm from '@/components/forms/ContactForm';
export default function ContactPage() {
return (
<div className="p-8 max-w-xl mx-auto">
<h1 className="text-2xl font-bold mb-4">Contact Us</h1>
<ContactForm />
</div>
);
}
🔹 src/components/forms/ContactForm.tsx
'use client';
import { submitContactForm } from '@/server/actions/contact';
export default function ContactForm() {
return (
<form action={submitContactForm} className="space-y-4">
<input
name="name"
type="text"
placeholder="Your Name"
className="border w-full p-2 rounded"
required
/>
<input
name="email"
type="email"
placeholder="Your Email"
className="border w-full p-2 rounded"
required
/>
<textarea
name="message"
placeholder="Your Message"
className="border w-full p-2 rounded h-32"
required
/>
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
Send
</button>
</form>
);
}
🔹 src/server/actions/contact.ts
'use server';
export async function submitContactForm(formData: FormData) {
const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');
console.log('Form submitted:', { name, email, message });
// Simulate saving to DB or sending an email
}
✅ Validation with Zod
Use Zod to validate form input on the server.
npm install zod
🔹 Updated Server Action with Zod
// src/server/actions/contact.ts
'use server';
import { z } from 'zod';
const ContactSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
});
export async function submitContactForm(formData: FormData) {
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
};
const result = ContactSchema.safeParse(rawData);
if (!result.success) {
throw new Error("Validation failed");
}
const { name, email, message } = result.data;
console.log('Validated:', name, email, message);
}
⚡ Optimistic UI Feedback (Client Side)
To give users instant feedback (like “sending…”), manage a loading state client-side.
🔹 Update ContactForm.tsx
'use client';
import { useTransition, useState } from 'react';
import { submitContactForm } from '@/server/actions/contact';
export default function ContactForm() {
const [isPending, startTransition] = useTransition();
const [message, setMessage] = useState('');
const handleSubmit = (formData: FormData) => {
startTransition(async () => {
await submitContactForm(formData);
setMessage('Message sent successfully!');
});
};
return (
<form action={handleSubmit} className="space-y-4">
{/* inputs same as before */}
<button type="submit" disabled={isPending} className="bg-blue-600 text-white px-4 py-2 rounded">
{isPending ? "Sending..." : "Send"}
</button>
{message && <p className="text-green-600">{message}</p>}
</form>
);
}
🔗 Submitting to API / Database (Optional)
You can also call your database (e.g. via Prisma) inside the server action:
// example
import { prisma } from "@/lib/prisma";
await prisma.contact.create({
data: { name, email, message }
});
Or send to a backend API using fetch()
from within your server function.
✅ Summary
You’ve learned how to:
Feature | Tool/Example |
---|---|
Server-side form submit | <form action={fn}> + use server |
Input validation | zod.safeParse() |
Optimistic feedback | useTransition() hook |
Backend save | Prisma, API, or mock DB inside server action |
🔜 Coming Up Next:
In Module 8, we’ll build and connect full Backend APIs in app/api/
, use Prisma ORM, and handle full CRUD operations in your Next.js 14 app.
Would you like this as Markdown or publish-ready HTML next?