build-contact-form-with-email
Builds a contact form that actually emails you, with a Next.js server action, server-side Zod validation, an email API such as Resend, a spam honeypot and success and error states. Use when the user wants contact form messages delivered to their inbox, an enquiry form, or a form that sends email.
Version 1.0 · Updated 2026-10-31
Install in Claude Code, Cursor or Codex (optional)
Only for AI coding tools that read skill folders. Run one line in a terminal, in your project folder. It downloads this skill's text files from TheSkillVault and nothing else; you can read them first with “View raw SKILL.md”.
Claude Code (all your projects)
curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/build-contact-form-with-email/raw' -o ~/'.claude/skills/build-contact-form-with-email/SKILL.md'Cursor (this project)
curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/build-contact-form-with-email/raw' -o '.cursor/skills/build-contact-form-with-email/SKILL.md'Codex and other Agent Skills tools (this project)
curl -fsSL --create-dirs 'https://theskillvault.ai/api/skills/build-contact-form-with-email/raw' -o '.agents/skills/build-contact-form-with-email/SKILL.md'
Or connect the whole library over MCP
Add TheSkillVault to Cursor or add this to your app's MCP settings (read-only; help):
{
"mcpServers": {
"theskillvault": {
"url": "https://theskillvault.ai/mcp"
}
}
}Build a contact form that emails you
When to use
- A website needs an enquiry or contact form whose messages land in the owner's inbox.
- An existing form "submits" but nothing arrives.
- For client-side field validation patterns on any React form, see
build-react-form-with-validation; this skill is about delivery.
Handoff
- Reads:
docs/website-brief.md(which fields the form asks for, the owner's email) andlib/site.ts. - Writes:
app/contact/page.tsx,app/contact/ContactForm.tsx,app/contact/actions.ts, and.env.examplelistingRESEND_API_KEYandCONTACT_TO(real values go in.env.localonly).
Steps
- Install:
npm install resend zod. Create a Resend account, add and verify the business domain at resend.com/domains (it gives you DNS records to add), and create an API key. .env.local(never committed):RESEND_API_KEY=...,CONTACT_TO=owner@business.com.au,CONTACT_FROM="Website <hello@your-domain.com.au>". Copy the names, not the values, into.env.example.app/contact/actions.tswith"use server"at the top:
- A Zod schema:
name(1-100 chars),email(z.email()in Zod 4,z.string().email()in Zod 3),message(10-5000 chars), plus any fields from the brief. export async function sendContact(prev: State, formData: FormData): Promise<State>.- Honeypot: if the hidden
companyfield has a value, return{ ok: true }without sending, so bots learn nothing. safeParsethe fields; on failure return{ ok: false, errors: z.flattenError(result.error).fieldErrors }(Zod 3:result.error.flatten().fieldErrors).- Send with
new Resend(process.env.RESEND_API_KEY).emails.send({ from: process.env.CONTACT_FROM, to: process.env.CONTACT_TO, replyTo: email, subject: \Website enquiry from ${name}\, text }). The SDK returns{ data, error }; checkerrorrather than relying on try/catch. - On error, log it on the server and return
{ ok: false, message: "Sorry, that didn't send. Please email us directly at …" }.
app/contact/ContactForm.tsx("use client"):const [state, formAction, pending] = useActionState(sendContact, { ok: false }). A<form action={formAction}>with a visible<label>for each input,type="email"andautoCompleteon email and name, andrequired.- The honeypot:
<div aria-hidden="true" className="hidden"><label>Company<input name="company" tabIndex={-1} autoComplete="off" /></label></div>. - States: disable the button and show "Sending…" while
pending; on success replace the form with a thank-you message in arole="status"region; show field errors next to each field (aria-describedby) and the general error inrole="alert". - A one-line collection notice under the button, e.g. "We use your details only to reply to you. See our privacy page." with a link to
/privacy. - Test locally: a valid message arrives, the reply goes to the sender, an invalid email shows the error, and a filled honeypot sends nothing. Add the same environment variables in the host's settings before deploying.
Gotchas
- Validate on the server even if the browser validates too; a bot can post anything to a server action.
onboarding@resend.devis Resend's testing sender only. Production mail must come from your verified domain, or it will bounce or land in spam.- Never put the visitor's address in
from; usereplyTo. Sending "from" their domain fails SPF/DMARC checks. - Don't put the API key in a client component or any
NEXT_PUBLIC_variable: it would ship to every visitor. - Treat the message as plain text. If you build HTML, escape the visitor's input first.
- A honeypot stops simple bots. If spam continues, add rate limiting or a CAPTCHA such as Cloudflare Turnstile.
Example prompt
"My contact form doesn't send anything. Make it email the enquiries to me with Resend and show a thank-you message."
Licence: CC BY 4.0. Attribution when you reuse it: From TheSkillVault, https://theskillvault.ai, CC BY 4.0