How to Send Email from a React App (2026 Guide)
How to send email from a React app: why the browser can't, the server-side options, React Email templates, and working Resend and Nodemailer code for 2026.

Ajay Sohmshetty

You can't send email directly from a React app in the browser. Browsers can't talk to mail servers, and any API key you ship to the client is public. Instead, the React side collects the input and calls a server-side endpoint (an API route, a serverless function, or your backend), which holds the provider's API key and sends the message. A common setup pairs that endpoint with an email API like Resend, Postmark, or SendGrid, with the template written in React Email.
Why can't you send email from the browser?
React runs in the browser, and the browser speaks HTTP. Email delivery uses SMTP, which the browser can't reach. There's no sendEmail() a component can call to reach a mail server.
Email APIs expect calls from a server. Resend's API doesn't support CORS, so a fetch from a component is blocked, and that's deliberate: the API key is the problem. Anything shipped to the browser is readable in devtools. Put RESEND_API_KEY in a NEXT_PUBLIC_ or VITE_ variable and you've published it. Someone can copy it, send email as you, burn your quota, and get your domain flagged for abuse.
So the rule is simple: the React side collects input and triggers an action, and a server-side piece holds the secret and sends.
What are the server-side options?
There are three common ways to add that server step.
| Approach | What you run | Deliverability handled by | Best for |
|---|---|---|---|
| Email API provider + SDK | An API route in your existing backend | The provider | Most apps; fastest setup |
| Provider SMTP via Nodemailer | A Node backend | The provider | Existing SMTP code or frameworks that expect SMTP |
| Serverless function | One function on your host | Whichever provider it calls | Client-only React apps with no backend |
Email API providers. Resend, Postmark, and SendGrid give you an HTTP API and an SDK. You call them from your server, and they handle SMTP, IP reputation, bounces, and retries. For most React apps this is the easiest path: one package, one environment variable, a few lines of code.
- Resend is maintained by the team behind React Email. Its free plan covers 3,000 emails a month (100 a day), and Pro is $20 a month for 50,000.
- Postmark focuses on transactional email and keeps it apart from broadcast mail using Message Streams. Free is 100 emails a month; Basic is $15 for 10,000.
- SendGrid scales to millions of emails a month but has no permanent free plan, only a 60-day trial at 100 emails a day. Essentials starts at $19.95.
Prices were checked in September 2026. For a deeper comparison, see the best email API providers.
SMTP through Nodemailer. You can also send over SMTP from a Node backend with Nodemailer. Point it at a provider's SMTP relay and you keep the provider's deliverability without its SDK. You can point it at your own mail server too, but a new server tends to land in spam until it builds a reputation, so save that for internal tools.
Serverless functions. If your React app has no server, a Vercel or Netlify function, a Cloudflare Worker, or a Supabase Edge Function is the smallest possible backend. It holds the key and runs the send. Your component calls fetch('/api/send') and the function does the rest.
This setup fits transactional email: codes, receipts, and account messages a user is waiting on.
How do you build the email with React Email?
Hand-coding email HTML is painful. Email clients lag far behind browsers, so layouts end up as nested tables with inline styles. React Email lets you write the email as a React component and renders it to HTML that email clients handle.
In React Email 6, components and render utilities all come from the react-email package. The official setup installs it with react and react-dom, plus @react-email/ui as a dev dependency for the local preview server:
npm install react-email react react-dom -E
npm install @react-email/ui -D -EA template looks like a normal component:
// emails/welcome.tsx
import { Body, Button, Container, Head, Html, Preview, Text } from 'react-email';
interface WelcomeEmailProps {
name: string;
}
export function WelcomeEmail({ name }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Your account is ready</Preview>
<Body>
<Container>
<Text>Hi {name}, your account is ready.</Text>
<Button href="https://example.com/start">Create your first project</Button>
</Container>
</Body>
</Html>
);
}
export default WelcomeEmail;To get HTML and a plain-text version, use render (it's async) and toPlainText:
import { render, toPlainText } from 'react-email';
import { WelcomeEmail } from './emails/welcome';
const html = await render(<WelcomeEmail name="Ada" />);
const text = toPlainText(html);For previews, add "email:dev": "email dev" to your package.json scripts and run npm run email:dev. It opens every template in your emails folder in the browser with hot reload, so you can check layouts without sending yourself test emails.
Because templates are components, you can share a Layout with your header, footer, and brand colors and change the footer once for every email.
A minimal working example with Next.js and Resend
The flow has two halves: a server route that holds the key and sends, and a React component that calls it. This example uses the Next.js App Router. The same logic ports to Express or a serverless function; in Express, read req.body and reply with res.status(...).json(...) instead of the Web Request and Response objects.
Install the SDK and store the key in a server-only variable (in Next.js, .env.local):
npm install resendRESEND_API_KEY=re_your_real_key_hereThe route. It contains JSX, so the file must end in .tsx:
// app/api/send/route.tsx
import { Resend } from 'resend';
import { render, toPlainText } from 'react-email';
import { WelcomeEmail } from '@/emails/welcome';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(request: Request) {
const { email, name } = await request.json();
if (typeof email !== 'string' || !email.includes('@')) {
return Response.json({ error: 'Enter a valid email address.' }, { status: 400 });
}
const html = await render(<WelcomeEmail name={String(name || 'there')} />);
const { data, error } = await resend.emails.send({
from: 'Acme <hello@yourdomain.com>',
to: [email],
subject: 'Welcome aboard',
html,
text: toPlainText(html),
});
if (error) {
console.error(error);
return Response.json({ error: 'We could not send the email. Try again.' }, { status: 500 });
}
return Response.json({ id: data?.id });
}A few things to notice:
- The key is read from
process.envon the server, so it never reaches the browser. - Resend returns
{ data, error }for API failures instead of throwing, so checkerror. - Resend also accepts the component itself through a
reactproperty and renders it for you. That path needs the@react-email/renderpackage to be resolvable from theresendpackage;react-emailinstalls it, but strict package managers like pnpm may require adding it directly. Rendering yourself, as above, works with any provider and gives you the plain-text part.
The React side collects input and posts to the route:
'use client';
import { useState } from 'react';
export function SignupForm() {
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
return (
<form
onSubmit={async (event)=> {
event.preventDefault();
setStatus('sending');
const form= new FormData(event.currentTarget);
const res= await fetch('/api/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: form.get('name'), email: form.get('email') }),
});
setStatus(res.ok ? 'sent' : 'error');
}}
>
<input name="name" placeholder="Name" />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={status= 'sending'}>
{status === 'sent' ? 'Check your inbox' : 'Sign up'}
</button>
{status === 'error' && <p>Something went wrong. Try again.</p>}
</form>
);
}That's a complete send path. For a Vite app with no server framework, the route becomes a standalone serverless function, and the split stays the same.
Sending over SMTP with Nodemailer
If you already have SMTP code, or a framework that expects an SMTP transport, use Nodemailer (version 10 at the time of writing) with your provider's SMTP relay. With Resend, the host is smtp.resend.com, the username is resend, and the password is your API key:
// lib/mailer.ts (server only)
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: 'smtp.resend.com',
port: 465,
secure: true, // implicit TLS; use port 587 with secure: false for STARTTLS
auth: {
user: 'resend',
pass: process.env.RESEND_API_KEY,
},
});
export async function sendWelcome(to: string, html: string, text: string) {
await transporter.sendMail({
from: 'Acme <hello@yourdomain.com>',
to,
subject: 'Welcome aboard',
html,
text,
});
}Pass it the html and text from React Email's render and toPlainText. You lose SDK conveniences like structured error responses, and provider features move into custom headers (Resend accepts a Resend-Idempotency-Key header over SMTP), but the code works with any SMTP provider.
What happens when you have more than one email?
Real apps need more than a welcome email: an order confirmation, a password reset, a nudge when someone stalls in onboarding. Each needs a trigger, a template, and rules for when it should and shouldn't fire. Put a resend.emails.send() call at every one of those spots, and a schema change or copy edit sends you hunting through routes and handlers.
Most of those events already show up as database changes. A signup is a new row in users. A paid order is a status change in orders. So instead of calling the email API from every handler, let the database change be the trigger:
- Postgres triggers or an outbox table. A trigger writes a row to an
email_outboxtable in the same transaction as the change, and a worker sends and marks it done. - Supabase Database Webhooks. Supabase can call an HTTP endpoint, such as an Edge Function, when a row is inserted or updated. The call is asynchronous, so it doesn't block the write.
- LISTEN/NOTIFY. A long-running worker subscribes to Postgres notifications and sends.
The payoff is reliability. When the send lives in the request handler, a provider outage can fail a signup that otherwise succeeded. With an outbox, the signup and its pending email commit together, and the worker sends the email as a separate job that it can retry on failure. Webhooks and LISTEN/NOTIFY also take the send out of the request, but they don't retry failed sends on their own. The cost is more moving parts to own. For the Supabase version of this, see how to send emails from Supabase.
Common mistakes to avoid
- Calling the provider from a client component. If browser code can send with your provider's secret API key, that key is exposed. Move the send server-side.
- Letting the client choose the recipient and content. A route that emails any address with any text is a spam relay. Validate input, fix the template on the server, and rate-limit the route.
- Sending from an unverified domain. Resend rejects sends from an unverified domain with a 403 error, and Gmail requires SPF or DKIM from every sender, so unauthenticated mail may go to spam or not arrive. Add your provider's SPF and DKIM records and verify the domain before launch. The email deliverability guide covers the setup.
- Skipping the plain-text part. Some clients and filters dislike HTML-only email.
toPlainTextgives you one from the same template. - Sending twice on retry. If a request retries, the email can go out twice. Resend accepts an idempotency key as a second argument, for example
resend.emails.send(payload, { idempotencyKey: 'welcome/' + userId }), and ignores repeats for 24 hours. - Blocking the response on the send. A slow provider makes your UI slow. For non-critical emails, queue the send and respond right away.
Where Dreamlit fits
This guide covers the send path. The emails that decide whether a new user sticks around, like onboarding nudges, trial reminders, and win-backs, are a different job. Dreamlit, the company behind this blog, does the activation work for you. Our experts pinpoint where users drop off and send the emails that turn them into paying customers, with the results on us. It fits products with 500 or more signups a month. For a contact form or a password reset, the code above is all you need.
Send path working and want to see where new users stall? See how Dreamlit works.
References
- React Email: Manual setup
- React Email: render utility
- Resend: Send email API
- Resend: Idempotency keys
- Resend: Send with SMTP
- Resend: API errors
- Resend pricing
- Postmark pricing
- SendGrid Email API pricing
- Nodemailer documentation
- Next.js: Route handlers
- Supabase: Database Webhooks
- EmailJS: Exposing your public key
- Google: Email sender guidelines
Frequently asked questions
Can you send email directly from a React app in the browser?
No. Browsers can't speak SMTP, and any email API key in client-side React code is visible to anyone who opens devtools. You need a server-side step, such as an API route, a serverless function, or a backend, that holds the key and sends. The React UI collects the input and calls that endpoint.
What is React Email and do I need it?
React Email is a library for writing email templates as React components instead of hand-coding HTML tables. In the current version (6.x) you import components like Html, Container, Text, and Button, plus the render and toPlainText utilities, from the react-email package. You don't need it, but it makes templates much easier to maintain than raw HTML strings.
What is the difference between React Email and Resend?
React Email builds the email's HTML. Resend, or another provider like Postmark or SendGrid, delivers it. The same team maintains both, but you can use React Email with any provider and Resend without React Email by passing an HTML string.
Which email provider should I use with React?
Resend has the closest React Email integration and a free plan of 3,000 emails a month. Postmark focuses on transactional email and keeps it separate from broadcast mail. SendGrid has no permanent free plan but scales to high volume. Pick based on volume, budget, and whether you want SMTP as well as an API.
How do I keep my email API key secret in a React app?
Never put it in client-side code or in a variable prefixed with NEXT_PUBLIC_ or VITE_, because those are bundled into the browser. Store it in a server-only environment variable like RESEND_API_KEY and read it inside an API route, server action, or serverless function.
Can I send email from a Vite or Create React App project with no backend?
Not with a secret API key. A client-only React app has nowhere safe to keep one. Add a serverless function on your host (Vercel, Netlify, or Cloudflare Workers), a Supabase Edge Function, or a small Express server, and have the React app call it. Browser-first services like EmailJS are the exception: the browser triggers templates you set up in their dashboard using a public key, so it can't send arbitrary content.
How do I send email when a database row changes instead of from my React code?
Let the database change be the trigger. Your app writes a row, and either a Postgres trigger adds an email job to an outbox table in the same transaction or a Supabase Database Webhook calls a function asynchronously. A worker or function then calls your email provider, so the user's request doesn't wait on the email. With an outbox, the worker can retry failed sends; webhooks don't retry on their own.
About the Author

Co-Founder & CEO
Ajay is Co-Founder and CEO of Dreamlit AI, which runs user activation for software companies. He was previously co-founder and CTO of Harmonic AI and a software engineer at Google. Full bio →
Other articles

Best Email API Providers in 2026: 8 APIs Compared
The best email API providers in 2026 for developers: Resend, Postmark, Amazon SES, SendGrid, Mailgun, Mailtrap, and more, compared on price, SDKs, and features.

What Is Transactional Email? Definition and Examples (2026)
Transactional email is a message your app sends to one person because of something they did, like a password reset or receipt. Definition, examples, and rules.