AI Reading
Quick summary of this article
Next.js 14 introduces Server Actions, a cleaner way to handle form submissions directly on the server without needing separate API routes. This module covers building forms with Server Actions, validating input using Zod, and adding optimistic UI feedback with the useTransition hook. You can also connect server actions to databases or external APIs for real backend integration.
- Server Actions use the
<form action={serverFunction}>syntax, where the function is marked with'use server'and receives form data as aFormDataobject. - Zod validation is applied on the server side using
zod.safeParse()to ensure inputs like name, email, and message meet required criteria before processing. - The
useTransition()hook enables client-side loading states (e.g., "Sending...") and success messages for a smoother user experience. - Server actions can integrate with databases (like Prisma) or call external APIs using
fetch()directly within the server function. - This approach eliminates the need for separate API route handlers, simplifying form logic and keeping code more organized.
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?
