Forms are everywhere – contact forms, newsletter signups, lead capture. Here’s how to handle them in a headless WordPress setup.
Approach 1: WordPress Forms (Recommended)
Use Contact Form 7 or WPForms in WordPress, submit from Next.js:
// Server action in Next.js
async function handleSubmit(formData: FormData) {
const response = await fetch(
'https://cms.flatwp.com/wp-json/contact-form-7/v1/contact-forms/123/feedback',
{
method: 'POST',
body: formData,
}
);
return response.json();
}
Pros:
- Editors manage form fields in WordPress
- Email notifications handled by WordPress
- Submissions stored in WordPress database
- Works with existing WordPress plugins
Cons:
- Extra API call to WordPress
- WordPress becomes a dependency for forms
Approach 2: Next.js Server Actions
Handle forms entirely in Next.js with server actions:
// app/contact/actions.ts
'use server'
export async function submitContact(formData: FormData) {
const data = {
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
};
// Send email via Resend, SendGrid, etc.
await sendEmail(data);
// Store in database if needed
await db.contacts.create(data);
}
Pros:
- No WordPress dependency
- Full control over validation and processing
- Modern server actions pattern
- Can integrate with any email service
Cons:
- Editors can’t manage form fields
- Need separate storage for submissions
- More code to maintain
Approach 3: Third-Party Services
Use Formspree, Tally, or similar:
<form action="https://formspree.io/f/your-id" method="POST">
<input name="email" type="email" />
<button type="submit">Submit</button>
</form>
Pros:
- Zero backend code
- Spam protection included
- Email notifications handled
- Nice dashboard for submissions
Cons:
- Monthly cost ($0-$20)
- Less customization
- Another service to manage
FlatWP’s Approach
We include adapters for all three approaches. Our default recommendation:
- Use WordPress forms for marketing sites (editors need control)
- Use server actions for apps (more dynamic needs)
- Use third-party for MVPs (fastest to ship)
The beauty of headless is you’re not locked in. Start with one, switch to another as needs change.
Validation with Zod
Regardless of approach, validate with Zod:
const contactSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
});
export async function submitContact(formData: FormData) {
const data = contactSchema.parse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});
// data is now typed and validated
}
Type-safe forms with runtime validation. Beautiful.