Backend · How to use a skill

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

View raw SKILL.md

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) and lib/site.ts.
  • Writes: app/contact/page.tsx, app/contact/ContactForm.tsx, app/contact/actions.ts, and .env.example listing RESEND_API_KEY and CONTACT_TO (real values go in .env.local only).

Steps

  1. 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.
  2. .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.
  3. app/contact/actions.ts with "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 company field has a value, return { ok: true } without sending, so bots learn nothing.
  • safeParse the 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 }; check error rather 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 …" }.
  1. 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" and autoComplete on email and name, and required.
  2. The honeypot: <div aria-hidden="true" className="hidden"><label>Company<input name="company" tabIndex={-1} autoComplete="off" /></label></div>.
  3. States: disable the button and show "Sending…" while pending; on success replace the form with a thank-you message in a role="status" region; show field errors next to each field (aria-describedby) and the general error in role="alert".
  4. 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.
  5. 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.dev is 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; use replyTo. 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