Next.js form submission without a backend you have to run
The Formward TeamFormward, Stockholm7 min read
The short answer: a Next.js form submission does not need a backend you build and operate yourself. You point the form at an external endpoint that accepts the POST, returns JSON, and stores the result, and your App Router app stays focused on rendering. This post walks through wiring a Next.js contact form backend two ways, a server action and a direct POST from the browser, and adds a simple spam field so you do not regret going public.
Start with the form itself. In the App Router, a contact form is just a React component in a page or a client component. You have a few text inputs, a textarea, and a submit button. The interesting decision is not the markup. It is what handles the submission once the user clicks the button, because that is where a backend usually appears. The goal here is to avoid standing up and babysitting that backend.
The first approach is a server action. You write an async function marked with the use server directive, pass it to the form's action prop, and Next.js runs it on the server when the form is submitted. Inside the action you read the fields off the FormData object, do whatever validation you want, and then forward the data to your form endpoint with a server-side fetch. Because the action runs on the server, your endpoint URL and any secret token never reach the browser, which is the main reason to prefer this style for a Next.js form submission that carries anything sensitive.
A server action that posts to an external endpoint looks like this in plain terms: read name, email, and message from FormData; bail out early with a returned error object if a required field is empty; then call fetch against your endpoint with a JSON body and an Accept header asking for JSON back. You await the response, check whether it was ok, and return a small object describing success or failure. The page reads that returned value with the useActionState hook and renders a confirmation or an error message. No database, no route handler, no server of your own to deploy.
The second approach is a direct POST from the browser. Here the form is a client component, you attach an onSubmit handler, call preventDefault, build a payload from the input values, and fetch your endpoint directly from the client. This is the classic backend-less pattern: the browser talks to the form service, and your Next.js app never sees the data server-side at all. It is simpler to reason about and it keeps your server doing nothing but serving HTML.
The tradeoff between the two is real and worth stating plainly. A server action hides the endpoint and lets you add server-side checks before anything is forwarded, but it means the submission round-trips through your Next.js server. A direct POST is leaner and works even on a fully static export, but the endpoint URL is visible in your client bundle. For most contact forms the endpoint being visible is fine, because the endpoint itself is the thing that enforces rate limits and spam rules. Choose the server action when you want to keep a token secret or run logic before forwarding; choose the direct POST when you want the absolute minimum of moving parts.
Whichever path you pick, send JSON and ask for JSON. A good form endpoint accepts a JSON body and, when you set the Accept header to application/json, replies with a JSON object instead of redirecting. That JSON response is what makes the in-page experience smooth: you get back a structured success flag or an errors object, and you render state from it rather than navigating away. Set Content-Type to application/json on the way out and Accept to application/json so the endpoint knows you want a machine-readable answer rather than a thank-you page.
Handle the response states explicitly, because users notice when you do not. Before the fetch resolves, show a pending state and disable the submit button so nobody double-submits. On a 2xx response, clear the fields and show a confirmation. On a non-ok response, read the JSON error and show it inline next to the relevant field or at the top of the form. On a network failure, catch the rejected promise and show a generic retry message. With a server action, useActionState gives you the pending flag and the returned value for free; with a direct POST, you track those states yourself with useState.
Now the spam field, which you want before you ship, not after the bots find you. The cheapest effective defence is a honeypot: add an extra input that a human will never fill in, hide it from real users with CSS rather than the hidden attribute, and give it an innocuous name like company or website. Many naive bots fill in every field they see. On the server action, or at your endpoint, you reject any submission where the honeypot field is non-empty. It costs you a few lines and catches a surprising amount of low-effort spam without showing your visitors a single puzzle.
A honeypot is not a silver bullet, so think about it as the first layer rather than the whole defence. Real spam protection lives at the endpoint: rate limiting so one source cannot flood you, content scoring so obviously junk messages get flagged, and an optional lightweight challenge for forms that attract determined attackers. Doing this well at the application layer is exactly the kind of work you were trying to avoid by not running a backend, which is the argument for letting the endpoint own it.
This is where a hosted form backend earns its place. Formward gives you a single endpoint to point your Next.js form at, accepts JSON, returns JSON when you ask for it, and runs the honeypot, rate limiting, and spam scoring for you. Submissions are stored in the EU and emailed to you, so a Next.js contact form backend becomes a one-line action attribute or a single fetch call rather than a service you deploy. You keep writing React; the endpoint handles the unglamorous parts.
There is a privacy angle worth keeping in view while you wire this up. A contact form collects personal data the moment someone types their email, so where that data lands matters under the GDPR. If your endpoint stores submissions outside the EU, you have quietly become a data exporter. Picking an endpoint that keeps the data in Europe means the residency question has a clean answer, and it costs you nothing extra in the Next.js code itself.
To put it together end to end: build the form as a normal component, decide between a server action and a direct POST based on whether you need to hide a token, send a JSON body with an Accept header for JSON, render pending, success, and error states from the response, and add a hidden honeypot field that the endpoint rejects. That is a complete Next.js form submission flow with no backend of your own to run, patch, or scale.
If you want to ship today, start with the direct POST version because it has the fewest parts, confirm the success and error states work against your endpoint, then add the honeypot. Move to a server action later only if you find you need to keep a token secret or run checks before forwarding. The form keeps working the same way for your users either way, and your App Router app stays a frontend, which is the point.