NEW AI agents now first-class: authorize · audit · revoke in one click — your agents submit cleanly, bots stay blocked. Read agent docs →

Remix form handling with a hosted backend (action + Form)

Remix's Form and route actions are made for this. Post to a hosted endpoint from your action for a complete contact form — progressive enhancement, no API.

Remix’s whole model — <Form> posts to a route action, which runs on the server — is exactly the shape a form wants. The only thing you’d normally hand-write is the part the action does: receive the submission, email you, filter spam. Hand that to a hosted endpoint and the action is a few lines. (This applies equally to React Router 7’s framework mode.)

The route action

// app/routes/contact.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";

const ENDPOINT = "https://login.ollastack.com/api/submit/your-slug";

export async function action({ request }: ActionFunctionArgs) {
  const data = await request.formData();
  const res = await fetch(ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      name: data.get("name"),
      email: data.get("email"),
      message: data.get("message"),
      _gotcha: data.get("_gotcha"),
    }),
  });
  if (!res.ok) return json({ ok: false }, { status: 502 });
  return json({ ok: true });
}

export default function Contact() {
  const result = useActionData<typeof action>();
  const nav = useNavigation();
  const sending = nav.state === "submitting";

  if (result?.ok) return <p>Thanks — we'll be in touch.</p>;

  return (
    <Form method="post">
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="you@example.com" required />
      <textarea name="message" placeholder="Message" required />
      <input type="text" name="_gotcha" tabIndex={-1} style={{ display: "none" }} />
      <button disabled={sending}>{sending ? "Sending…" : "Send"}</button>
      {result?.ok === false && <p>Something went wrongtry again.</p>}
    </Form>
  );
}

<Form method="post"> works without JavaScript (a real POST + full reload), and Remix progressively enhances it to a no-reload submit once JS loads — useNavigation() gives you the pending state, useActionData() the result. No fetch boilerplate in the component, and no backend of your own.

Notifications, spam, webhooks

  • Notifications: set the recipient in form settings (verify it first — a form only notifies verified addresses). The email field becomes Reply-To automatically.
  • Spam: a layered pipeline runs automatically; a real lead is never silently dropped (ML-uncertain submissions are delivered and labeled [Possible spam], recoverable in one click).
  • Webhooks: forward to Slack/Discord or your own signed endpoint with retries + replay.

Next steps

  • File uploads: forward request.formData() as multipart/form-data (the endpoint accepts it).
  • Let an agent submit with a scoped Bearer token; it reads the API from /api/openapi.json.

Create your form — 100 submissions/month free, no card.

Frequently asked questions

How do I handle a form in Remix?

Use Remix's Form component and a route action that POSTs to a hosted form endpoint. You get a complete contact form with progressive enhancement and no API to build.

Does it work without JavaScript?

Yes — Remix's Form plus route action gives progressive enhancement, so submissions work even before JS hydrates.

Is spam handled?

Yes — the hosted endpoint runs the spam pipeline; add a honeypot field too.

Last updated June 19, 2026. Spotted something out of date? Email hello@ollastack.com.