Sending Email from a React App: A Backend Problem in Disguise

"When someone signs up, just send them a welcome email." It's the kind of request that gets tossed into a standup like it's nothing, and the person saying it genuinely believes it's a five-minute task. Send an email. How hard could it be?
Here's the thing that makes email sneaky: the part you'd expect to be hard is trivial, and the part you'd expect to be trivial is where the whole thing lives. Triggering an email from your app is a single function call. Actually getting that email into someone's inbox — not their spam folder, not bounced, not silently dropped — is a genuinely deep backend problem involving deliverability, sender reputation, authentication protocols, and a rendering environment stuck two decades in the past. And almost none of it is a React problem, which is exactly why it catches frontend developers off guard.
So this article is a map of where the real work is. Because the most useful thing I can tell you about email in a React app is which parts to not do yourself, and why the boundary sits where it does.
The Line: The Client Never Sends Email
Start with the hard boundary, because it's the one people are tempted to cross. Your React app does not send email. Ever. It triggers a request to your backend, and your backend sends the email.
This isn't a style preference, it's a security wall. Sending email requires credentials — API keys for your email service — and anything in your React bundle is public. Ship that key to the client and you've handed the entire internet the ability to send email as you, which means spammers now have a free megaphone wearing your domain's reputation. Within days your domain is blacklisted and even your legitimate mail stops arriving. So the flow is always the same shape as payments and auth: the client asks, the server acts, and the credential never leaves the server.
// The client's entire involvement: ask the backend to do it. async function requestWelcomeEmail(userId: string) { await fetch('/api/emails/welcome', { method: 'POST', body: JSON.stringify({ userId }), }); }
That's the whole frontend story. There is no sendEmail in your components, no email API key in your environment variables with a public prefix, no SMTP in the browser. The moment email touches the client, you've done it wrong. Everything interesting from here happens on the server.
Why You Don't Talk to SMTP Yourself
On the backend, the naive mental model is that you connect to a mail server over SMTP and send. You can do that. You shouldn't, and understanding why is the core of this whole topic.
Getting an email delivered is not about transmitting it — it's about being trusted enough that the receiving server puts it in the inbox instead of the spam folder or the trash. That trust is a system: your domain needs authentication records (SPF, DKIM, DMARC) that prove you're allowed to send as your domain; your sending IP needs a reputation built over time; you need to handle bounces and complaints so you stop mailing dead addresses; and you need to not trip the spam filters that treat unknown senders with suspicion. Running your own mail server means owning all of that — reputation, blacklist monitoring, authentication, the works — and it is a full-time infrastructure job that has nothing to do with your product.
This is why transactional email services — Resend, Postmark, SendGrid — exist, and why using one isn't laziness but the correct architecture. They've spent years building sender reputation, they handle the authentication protocols, they manage bounces and deliverability, and they give you a clean API. Your backend calls that API; the hard, invisible work of actually getting delivered is theirs.
// Your server. The service owns deliverability; you own intent. import { Resend } from 'resend'; const resend = new Resend(process.env.RESEND_API_KEY); // server-only secret app.post('/api/emails/welcome', async (req, res) => { const user = await getUser(req.body.userId); await resend.emails.send({ from: '[email protected]', to: user.email, subject: 'Welcome aboard', react: <WelcomeEmail name={user.name} />, // more on this below }); res.json({ queued: true }); });
Same lesson as every article in this series: the raw capability (SMTP) is a footgun, and the value is a service that owns the messy, reputation-dependent edges. You are buying deliverability, not a send button.
The Genuinely Weird Part: Email Rendering Is Stuck in 2005
Here's the one place your React skills partly transfer, and it comes with a nasty surprise. You'd assume you can build an email template with normal components and modern CSS. You cannot, and this trips up every frontend developer exactly once.
Email clients — Outlook especially, but not only — render HTML with engines that are decades behind browsers. No flexbox, no grid, no modern CSS, inconsistent support for things you consider baseline. The reliable way to lay out an email is tables, the way the web worked in 2005, with inlined styles because <style> blocks get stripped. Hand-writing that is miserable and easy to get wrong across the dozens of clients your users actually use.
The modern rescue is that you can write email templates as React components — using a library like React Email — and it compiles them down to the table-based, inline-styled HTML that survives Outlook. You get to think in components; the library deals with the 2005 rendering reality underneath.
import { Html, Button, Text } from '@react-email/components'; export function WelcomeEmail({ name }: { name: string }) { return ( <Html> <Text>Hi {name}, glad you're here.</Text> <Button href="https://yourdomain.com/start">Get started</Button> </Html> ); }
That looks like a normal component, but <Button> and <Text> aren't rendering <button> and <p> — they emit the gnarly, table-wrapped, inline-styled markup that renders consistently from Gmail to a decade-old Outlook. It's the same pattern once more: a library absorbing an ugly reality so you can work in a sane abstraction. You write React; it ships something that would make you cry if you had to write it by hand.
How I'd Approach It
Strip it to decisions:
- The client never sends email. It asks the backend. The API key is a server-only secret — leaking it burns your domain's reputation.
- Don't run your own mail server. Deliverability is a reputation-and-authentication system, not a transmission problem. Use a transactional email service.
- You're buying deliverability, not a send button. SPF/DKIM/DMARC, bounce handling, and IP reputation are the actual product these services sell.
- Email rendering is stuck in the past. Write templates as React components via React Email; let it compile down to the table-based HTML that survives Outlook.
The reason "just send an email" turns into a week is that the sentence hides the entire iceberg. The tip — trigger a send — is the trivial thing everyone pictures. The mass underneath is deliverability, authentication, reputation, and a rendering environment that predates most of the developers working on it. The good news is that the whole iceberg is a solved, buyable problem: a transactional service for delivery, React Email for templates, and a hard rule that none of it touches the client. Get the boundary right and email goes back to being the five-minute task everyone assumed it was — because someone else already did the hard 95%.
Next, the series shifts from wiring things up to thinking about them differently: design prototyping for React — where design work stops being visual and starts being architectural, and why that handoff matters more than people admit.
If you've got emails landing in spam, or a template that looks perfect everywhere except Outlook, those are the two classic email potholes — deliverability and rendering — and both have known fixes. Tell me which one's biting you and I'll point you at the layer to check.