Serverless forms: write a function or use an endpoint?
If your site already deploys to Vercel, Netlify, Cloudflare or AWS, a form handler looks like a twenty-line function. Sometimes it is, and then writing it yourself is the right call. More often the twenty lines grow into spam filtering, email deliverability, storage, retries and deletion rules. This page sets out when each approach fits, including the cases where we would tell you to keep your own function.
Short version: if the submission has to touch your own database or business logic in the same request, write the function. If the form is there to reach a person, an endpoint is less to build and less to maintain.
What a form function actually involves
The first version is short. Here is roughly where it starts:
// A typical form function (Web-standard Request/Response).
export default async function handler(req: Request): Promise<Response> {
if (req.method !== "POST") return new Response(null, { status: 405 });
const data = Object.fromEntries(await req.formData());
if (data._gotcha) return Response.redirect("https://example.com/thanks", 303);
if (typeof data.email !== "string" || !data.email.includes("@")) {
return new Response("Please enter an email address", { status: 422 });
}
// Still to write: rate limiting, origin checks, storage,
// an email provider with SPF and DKIM, retries when it fails,
// a place to read old messages, and deletion after N days.
await sendEmail({ to: "team@example.com", subject: "New message", body: data });
return Response.redirect("https://example.com/thanks", 303);
}The comment is where the work is. A form that reaches real people also needs:
- Spam defence. A honeypot catches simple bots. Repeat senders need a rate limit, and a rate limit needs somewhere to count, which a stateless function does not have.
- Email that arrives. An account with an email provider, a verified domain with SPF and DKIM, and handling for the day the provider rejects a send.
- A copy you can find. Email alone loses messages to spam folders and deleted inboxes. You need storage and a way to search it.
- Deletion. A retention period under the GDPR means a scheduled job that deletes old submissions, and a way to find one person's messages when they ask.
- Upkeep. Runtime versions reach end of life, dependencies need updates, and someone has to notice when the function starts failing.
When your own function is the better choice
Write the function, or keep the one you have, when:
- The submission must be written to your own database together with other changes, for example creating an account or reserving stock.
- Validation depends on your data: checking an order number, a voucher or whether a time slot is still free before you accept the form.
- The handler needs secrets or internal services that must not be reachable from a third party.
- You already run a backend with email, storage and monitoring, so the extra work is small, and adding another processor to your records is not worth it.
- Your policy says personal data from this form stays inside your own infrastructure. A hosted endpoint, wherever it is hosted, is a processor you have to list and sign a DPA with.
When a hosted endpoint is less work
- The form exists to reach a person: contact, quote requests, applications, event sign-ups.
- The site is static, or built by someone who should not have to maintain backend code. See static site forms.
- You run forms on several sites and want them in one dashboard, with people who are not developers able to read and answer them.
- You want spam layers, retention, search, export and data subject request tools without building each one.
The trade-off is real: you depend on a provider, its plan limits and its uptime, and it becomes a processor of your visitors' data. With Formward that processor stores data in Sweden, under a published DPA.
Using both: endpoint first, function second
You do not have to choose one. Let the browser post to the endpoint, then add a webhook that calls your function:
<!-- The browser posts to the endpoint; your function gets a signed webhook. -->
<form action="https://forms.formward.eu/f/YOUR_FORM_ID" method="POST">
...
</form>Spam never reaches your function, because webhooks only fire for processed submissions. With a signing secret set, each delivery is signed with HMAC-SHA256. It is retried if your function fails, and the submission is stored and emailed even when your function is down. Your function keeps the business logic and nothing else.
Proxying through a function: the catches
The opposite setup, where the browser posts to your function and the function forwards to Formward, works but loses some things:
- Every request comes from your function's IP address, so the per-IP rate limit counts all your visitors as one sender. Raise the form's limit or expect 429s under load.
- A server-side request has no
Originheader unless you set one. With an allowed-origins list in place, requests without a matching origin get a 403. - The stored IP hash describes your function, not the visitor.
If you only need the function to add a value or check something first, the contact form API from the browser plus a webhook is usually the simpler route.
Where the data lives
Formward stores submissions, file attachments and backups in Sweden, on servers run by Hostup AB. Notification email goes out through Mailjet from an EU data centre. Submitter IP addresses are replaced with a keyed HMAC-SHA-256 hash on receipt, so the raw address is never written to disk. On Professional and Business, AI features run on Mistral AI in France and can be switched off per form.
Data leaves the EU only where you send it: a notification inbox hosted outside the EU, a webhook URL, a Slack channel or a Google Sheet. Cloudflare Turnstile is a US service, and it is off unless you switch it on for a form. The details are in the sub-processor register, the data residency page and the published DPA. For the wider picture, see GDPR form backend.
Questions
- Is a serverless function cheaper than a form service?
- For the compute alone, often yes: a contact form uses very little. The cost is in the code around it and the time to keep it working. Formward's Free plan covers 50 submissions a month, so for a small site the comparison is mostly about upkeep, not invoices.
- Can I run my own logic and still use Formward?
- Yes. Let the browser post to the endpoint and add a webhook that calls your function. Formward handles spam, storage and email, and your function receives only processed submissions, signed with your secret. Webhooks start on Professional.
- Does a function keep the data in my own infrastructure?
- Only if every service it calls does too. The function's logs, the email provider and the database are each a place the submission lands. Check the region of each one, and whether the logs contain request bodies.
- What about edge functions?
- The same trade-offs apply. Edge runtimes run close to the visitor, which helps little for a form, and by default they run wherever the request lands, so pin the region if data location matters to you.
Skip the function for your next form
Point the form at an endpoint and see whether you still miss the function. Free, no card needed.