Send email with React
A contact form that sends from the component, with a key that's safe to ship — or a small backend route when you have one.
Facts verified 2026-07-17 — corrections: hello@emailfast.dev
A React component can send email without any backend at all. Email Fast ships a browser SDK with EmailJS-compatible endpoints — the recipient always comes from the server-stored template, never from the request, so a public key in your frontend can't be abused to spam arbitrary addresses. Initialize it with your public key, point it at a server-stored template, wire it to a form. If you already run a backend, skip the browser SDK and send from there instead — that variant is below.
npm install @email-fast/browser// ContactForm.jsx
import { useState } from "react";
import emailfast from "@email-fast/browser";
emailfast.init({ publicKey: "ef_pub_..." }); // public by design — safe to ship
export function ContactForm() {
const [state, setState] = useState("idle");
async function onSubmit(e) {
e.preventDefault();
setState("sending");
try {
await emailfast.sendForm("default", "contact_form", e.target);
setState("sent");
} catch {
setState("error");
}
}
return (
<form onSubmit={onSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button disabled={state === "sending"}>Send</button>
{state === "sent" && <p>Thanks — we got it.</p>}
{state === "error" && <p>That didn't go through. Try again.</p>}
</form>
);
}"contact_form" is a template slug: the template — including who receives the mail — lives in your dashboard, not in the component.
Or: send through your own backend
If your React app already talks to a server, keep the secret key there and expose one route:
// server.js — Express; the secret key never reaches the browser
import express from "express";
import { EmailFast } from "@email-fast/nodejs";
const app = express().use(express.json());
const ef = new EmailFast({ apiKey: process.env.EMAILFAST_API_KEY, baseUrl: "https://api.emailfast.dev" }); // ef_sandbox_… while testing
app.post("/api/contact", async (req, res) => {
try {
const { id } = await ef.send({
to: "team@yourdomain.dev",
subject: "New contact message",
html: "<p>{{message}}</p>",
data: { message: req.body.message },
idempotency_key: req.body.formId, // generated once per render — a double-submit replays
});
res.status(202).json({ id });
} catch {
res.status(502).json({ error: "send failed" });
}
});Gotchas
- Never put an
ef_live_secret key in React code. Bundlers inline every env var you reference —VITE_…andREACT_APP_…are configuration, not secrets. The only key that may ship in a bundle isef_pub_. - Form field
nameattributes become template variables:name="email"fills{{email}}in the template. - Disable the submit button while sending —
sendFormreads the form at call time, and the browser call has no idempotency key to dedupe a double click. - Changing the recipient is a dashboard change, not a code change. That is the point.
Next
- Browser quickstart — templates,
sendForm, migrating from EmailJS - Frontend email without a backend
- The email API — for the backend variant
- All docs