← Back to blog

A React form backend with fetch and no server to run

The Formward TeamFormward, Stockholm6 min read

You can give a React form a real backend without running a server at all. The trick is that the backend is an external endpoint you POST to with fetch, so your React app stays a pure frontend while submissions are still validated, stored, and emailed somewhere reliable. This post builds a controlled React form, wires a fetch POST to a form endpoint, and handles the success and error states properly, with nothing for you to deploy or keep alive.

First, the controlled form. In React, a controlled form keeps each field's value in component state and updates it on every change. You hold the values in a single state object, render each input with its value bound to that state and an onChange handler that writes back, and you now have the form's data available in memory at submit time. Controlled inputs are slightly more code than uncontrolled ones, but they make validation, clearing, and disabling trivial, which is exactly what you need around a network request.

Keep the state shape simple: one object with keys for name, email, and message, plus separate pieces of state for status and error. Status is a small string state machine with values like idle, submitting, success, and error. Tracking status as one value rather than several booleans keeps your render logic honest and stops you from showing a spinner and a success message at the same time.

Now the submit handler, which is where the React form backend actually lives. On submit you call preventDefault so the browser does not navigate, set status to submitting, and call fetch against your form endpoint. The fetch uses the POST method, sets Content-Type to application/json, sets Accept to application/json so the endpoint returns JSON instead of redirecting, and sends the form state serialized as the body. You await the response and branch on whether it was ok.

Sending JSON and asking for JSON back is the detail that makes this pleasant. A good form endpoint, when it sees the Accept header requesting application/json, replies with a structured object rather than a thank-you page. That means after the await you can read a success flag or an errors object and drive your UI from it, instead of losing the user to a full-page redirect. If you ever see your page navigate away on submit, it is almost always because the Accept header is missing.

Handle success deliberately. When the response is ok, set status to success, clear the form state back to empty so the inputs reset, and render a confirmation message in place of the form or above it. Clearing the controlled state is a one-liner precisely because the form is controlled, which is the payoff for the extra wiring earlier. Give the user something concrete, like a line confirming you will reply by email, rather than a bare checkmark.

Handle errors with the same care, because this is where most React form backend implementations quietly fall short. There are two failure shapes to cover. The first is a response that comes back not ok, for example a 422 with validation errors; read the JSON body, pull out the field errors, and render them next to the right inputs. The second is the fetch itself rejecting, which happens on network loss or a blocked request; wrap the fetch in a try and catch the rejection, then set status to error and show a generic retry message. Without the catch, a dropped connection leaves your form stuck in the submitting state forever.

While the request is in flight, reflect it in the UI. Disable the submit button whenever status is submitting so an impatient user cannot fire three submissions, and change the button label to something like Sending so the wait is visible. Re-enable the button on success or error. These are tiny touches, but they are the difference between a form that feels solid and one that feels broken under a slow connection.

A note on validation: do the cheap checks on the client and treat the endpoint as the real authority. Client-side checks like a required name or a vaguely email-shaped string give instant feedback and save a round trip, but they are trivially bypassed, so they are a convenience, not a guarantee. The endpoint must validate again on its side. The nice consequence is that you can keep your React validation light and lean on the endpoint to reject the genuinely bad input, which keeps your component small.

This whole pattern depends on the endpoint being good, and that is the part worth not building yourself. You want it to accept JSON, return JSON, rate-limit floods, filter spam, store submissions somewhere durable, and email you when one arrives. Formward provides exactly that: you point the fetch at a Formward endpoint and get JSON responses, spam filtering, and EU-based storage, so the React form backend is a single fetch call from a frontend you already have. There is genuinely no server for you to run.

Because a form collects personal data the instant someone types their email address, where the endpoint stores that data is not a detail you can ignore under the GDPR. If submissions land outside the EU, your frontend has made you a data exporter without you writing a line of backend code. Choosing an endpoint that keeps the data in Europe means that question answers itself, and it changes nothing about the React code you write.

Add a simple spam defence while you are here, since a public form attracts bots within days. The lightest one is a honeypot field: render an extra input, hide it from real users with CSS, give it a name a bot would happily fill, and include its value in the payload. The endpoint rejects any submission where that field is non-empty. It is a few lines in your controlled state object and it removes a meaningful chunk of automated junk before it ever reaches you.

Putting it together, the full shape is small: controlled inputs bound to a state object, a status state machine, a submit handler that POSTs JSON with an Accept header for JSON, success that clears the form and confirms, errors that cover both not-ok responses and rejected fetches, and a disabled button while submitting. That is a complete React form backend with no server to run and no infrastructure to maintain.

Ship the happy path first and test it against your real endpoint, then deliberately break things: submit with the network throttled to zero, submit with a missing required field, double-click the button. If each of those produces a clear, recoverable state, your form is done. The form is just a frontend; the endpoint is the backend; and you never had to run a server to get there.

About the author

The Formward Team builds privacy-first form infrastructure in Stockholm. Read about our security and privacy practices. Our approach follows the principles set out by the European Data Protection Board at edpb.europa.eu.

A React form backend with fetch and no server to run | Formward