Chapter 8 of 11
Forms are how users send data to your app — login forms, signup forms, search bars, contact forms. Next.js gives you two main approaches: Server Actions (the modern way) and client-side form handling.
ANALOGY
Real-world analogy: Filling out a form at a bank. You fill out a paper form (the frontend), hand it to the teller (the form submit), and they take it to the back office to process it (the server). Server Actions let you process form data directly on the server — without writing a separate API endpoint. The form talks directly to your server code.
// No "use client" — this whole file runs on the server
export default function ContactPage() {
// The "use server" directive marks this as a Server Action
// It runs on the server when the form is submitted
async function sendMessage(formData) {
"use server";
// formData.get() reads the form field values
const name = formData.get("name");
const email = formData.get("email");
const message = formData.get("message");
// These lines run on the server — you can safely use a database or email API
await db.saveMessage({ name, email, message });
await emailService.send({ to: email, subject: "We received your message!" });
console.log(`New message from ${name}`);
}
return (
// Pass the server action directly to the form's action prop
<form action={sendMessage}>
<input name="name" placeholder="Your name" required />
<input name="email" type="email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit">Send Message</button>
</form>
);
}"use client";
import { useState } from "react";
export default function SignupForm() {
const [errors, setErrors] = useState({});
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
async function handleSubmit(e) {
e.preventDefault(); // Stop the browser's default form submission
const data = Object.fromEntries(new FormData(e.target));
// Validate before sending to server
const newErrors = {};
if (!data.email.includes("@")) {
newErrors.email = "Please enter a valid email address";
}
if (data.password.length < 8) {
newErrors.password = "Password must be at least 8 characters";
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return; // Stop here — don't submit with invalid data
}
setLoading(true);
const res = await fetch("/api/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
setLoading(false);
if (res.ok) setSuccess(true);
}
if (success) return <p>✅ Account created! Welcome aboard.</p>;
return (
<form onSubmit={handleSubmit}>
<div>
<input name="email" type="email" placeholder="Email address" />
{errors.email && <p style={{color: "red"}}>{errors.email}</p>}
</div>
<div>
<input name="password" type="password" placeholder="Password" />
{errors.password && <p style={{color: "red"}}>{errors.password}</p>}
</div>
<button type="submit" disabled={loading}>
{loading ? "Creating account..." : "Create Account"}
</button>
</form>
);
}