All resources
Static site forms

How to Add a Contact Form to a Vercel Site

Vercel has no forms product, so a contact form means writing a function or pointing it elsewhere. The five things the DIY path really needs, and the one-line alternative.

The ShipMyForm team

· 5 min read

Vercel has no forms product. Unlike Netlify, there is nothing to enable: your form posts into the void until you give it somewhere to go. You have two options. Write a Vercel Function or a Next.js Server Action and own the email, spam, and rate limiting yourself, or point the form's action at an external form endpoint and write no backend at all:

html
<form action="https://shipmyform.com/f/YOUR_FORM_ID" method="POST">
  <input type="text" name="name" required />
  <input type="email" name="email" required />
  <textarea name="message" required></textarea>
  <!-- honeypot: bots fill it, humans never see it -->
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" />
  <button type="submit">Send</button>
</form>

Full disclosure: ShipMyForm is our product. The function route below is written to be actually usable, not as a strawman, and there is a section on when it is the better choice.

Why there is no easy answer here

Netlify detects a form at build time and captures submissions for you. Vercel does not do this and has never offered it. Vercel's own answer to "how do I handle a form" is Vercel Functions, and its marketplace lists third-party form backends for people who would rather not write one.

So the question is not which Vercel setting do I turn on. It is who writes the submission handler.

Option 1: a Next.js Server Action

If you are on Next.js, this is the path the framework points you at. The action runs on the server, so your email API key never reaches the browser.

tsx
// app/actions.ts
"use server";

export async function sendContact(initialState, formData) {
  const email = String(formData.get("email") ?? "");
  const message = String(formData.get("message") ?? "");
  if (!email.includes("@") || message.length < 10) {
    return { message: "Please check your details." };
  }

  // your transactional email provider goes here
  await sendEmail({ to: "[email protected]", from: "[email protected]", text: message });
  return { message: "Thanks, we'll be in touch." };
}
tsx
// app/contact-form.tsx
"use client";

import { useActionState } from "react";
import { sendContact } from "./actions";

export function ContactForm() {
  const [state, formAction, pending] = useActionState(sendContact, { message: "" });
  return (
    <form action={formAction}>
      <input type="email" name="email" required />
      <textarea name="message" required />
      <p aria-live="polite">{state.message}</p>
      <button disabled={pending}>Send</button>
    </form>
  );
}

That is the tutorial version, and it works. Here is what the tutorials leave out.

The action is a public endpoint

It is tempting to read a Server Action as a private function call, because there is no visible API route. It is not. Next.js documentation is blunt about this: an action runs as a POST request against the page and is reachable by anyone who can send the same POST, so you should treat every action as an untrusted entry point.

Next.js does compare the request's Origin against the Host and reject mismatches, which stops a browser on another site from firing your action. It does not stop curl, or a script that sets whatever headers it likes. Once a bot finds the endpoint, every submission it sends runs your handler and, if you have not checked, sends you an email.

Rate limiting does not work the way you expect

The obvious fix is a per-IP counter. On Vercel, this is the usual first attempt:

js
// This does not work.
const seen = new Map();

Functions are stateless and scale out to many parallel instances, so that map is per-instance and empty again after a cold start. A bot spreading requests across instances never trips it. Real limiting needs shared state, which means Redis or a rule in Vercel's WAF. That is another service, another account, and another set of keys, for a contact form.

Deploys can break a form that is already open

Each Server Action has a build-time ID, and new deployments generate new ones. Next.js rotates them at least every 14 days even when nothing changed. A visitor who opened your contact page before a deploy and submits after it can hit an action ID that no longer exists, producing a "Failed to find Server Action" error. It is recoverable with a refresh, but the visitor has to know that, and a contact form is exactly the kind of page people leave open.

The Hobby plan bars commercial use

This is the one that catches agencies. Vercel's documentation states plainly that the Hobby plan restricts users to non-commercial, personal use only. A contact form on a client site, a business site, or anything that sells is commercial use, and the Pro plan starts at 20 dollars per user per month.

None of this makes Server Actions wrong. It means a production contact form built this way is a small project, not a snippet.

Option 2: a Vercel Function

On Astro, SvelteKit, Nuxt, or plain HTML, the same work lives in a function instead. The trade-offs are identical: you still write the validation, the spam defense, the rate limiting, and the email sending. Two platform limits worth knowing, current as of September 2026: a function's request body is capped at 4.5 MB, and a Server Action's body defaults to 1 MB, which matters the moment your form accepts file uploads.

What the DIY path actually costs

"Just write a function" is one line of advice that expands into five pieces of infrastructure:

What you needWhat it means on Vercel
Send the emailA transactional email API, plus SPF, DKIM, and DMARC records on your sending domain. Vercel does not send email.
Stop spamHoneypot and timing checks you write, plus a CAPTCHA or content filtering when the bots get past them.
Rate limitExternal shared state (Redis) or a WAF rule, because functions are stateless.
Keep the submissionsA database, or nothing: a failed email delivery means the lead is simply gone.
Retry failed deliveriesA queue, or accept silent loss when your email provider has a bad minute.

Each is a solved problem, and each is a service, a key, and a thing that can break on a Friday.

Option 3: point the form somewhere else

A form backend is a URL that does all five. Your Vercel project stays static, which also sidesteps the Hobby commercial-use question, because the form posts off-platform.

The markup at the top of this page is the whole integration. To submit without a page reload, post it with fetch instead:

js
const res = await fetch("https://shipmyform.com/f/YOUR_FORM_ID", {
  method: "POST",
  body: new FormData(form),
  headers: { Accept: "application/json" },
});
if (res.ok) form.outerHTML = "<p>Thanks, we'll be in touch.</p>";

Nothing here is Next.js-specific, so the same markup works on Astro, SvelteKit, Nuxt, or a hand-written HTML page on Vercel.

Which one should you pick?

  • The submission has to do something custom such as writing to your database, calling your API, or branching on business logic → write the function. A form backend delivers a submission; it does not run your code.
  • You already have Redis, an email provider, and a queue → you have paid the setup cost. Use them.
  • You want a contact form, not a subsystem → point it at an endpoint and spend the afternoon on something else.
  • Your Vercel project is a static marketing site on Hobby → keep it static. An external endpoint keeps you out of Function invocations and out of the commercial-use clause.
Verify before you commit:

Vercel's plan limits and terms were read from their documentation in September 2026, and the Next.js behaviour from the 16.2 docs shipped with this site. Platforms change both; confirm on vercel.com/docs/limits and the Hobby plan page before making a decision that depends on them.

Next steps

Frequently asked questions

Does Vercel have a built-in forms feature?
No. Unlike Netlify, Vercel has no forms product: there is nothing to enable, and a form on a Vercel site does nothing on submit until you give it somewhere to POST. Your two options are writing a Vercel Function (or a Next.js Server Action) that handles the submission yourself, or pointing the form's action at an external form endpoint, which needs no backend code.
Can I use a contact form on Vercel's free Hobby plan?
For a personal site, yes. But Vercel's documentation states that the Hobby plan restricts users to non-commercial, personal use only. A contact form on a business site, an agency site, or anything that sells is commercial use, which means the Pro plan at 20 dollars per user per month. An external form endpoint avoids this entirely, because the form posts off-platform and your Vercel project stays a static site.
Is a Next.js Server Action safe to use for a public contact form?
It works, but it is not private. Next.js documentation is explicit that a Server Action runs as a POST request against the page and is reachable by anyone who can send that POST, so it should be treated as an untrusted entry point. Next.js checks that the Origin matches the Host, which stops cross-site browser requests but not a script that sets its own headers. Validation, spam filtering, and rate limiting are still yours to write.
Why doesn't my rate limiting work on Vercel?
Because functions are stateless and scale to many parallel instances, so a counter held in a module-level variable is per-instance and resets on cold starts. Effective per-IP limiting on Vercel needs shared external state, usually Redis, or a rule in Vercel's WAF. This is the step most contact form tutorials leave out.
How do I send the email from a Vercel contact form?
Vercel does not send email, and you cannot use SMTP credentials safely from client-side code. You need a transactional email API with a verified sending domain, which means DNS records for SPF, DKIM, and DMARC, plus an API key in your environment variables. A form backend does this part for you and sends from its own authenticated domain.
Does this work with Astro, SvelteKit, or a plain HTML site on Vercel?
Yes. Pointing a form's action attribute at an external endpoint is plain HTML, so it works the same on any framework Vercel hosts, and on a fully static project with no functions at all. The Server Action route is Next.js-specific; every framework on Vercel can instead use a function or an external endpoint.

Related guides