Transactional Email Nobody Gets Right (And The Stack That Finally Fixes It)

Programming· 6 min read

Transactional Email Nobody Gets Right (And The Stack That Finally Fixes It)

The moment I realized I was doing transactional email wrong was when a user messaged me on LinkedIn saying they hadn't been able to reset their password for three days.

Three days.

And I had no idea.

It wasn't a server problem. It wasn't a bug in the auth logic. It was that my password reset email was landing in spam — or not arriving at all — because I'd set up the system the way everyone does: fast, with `nodemailer`, at 2AM, without thinking about deliverability or maintainability.

That day I decided I'd never improvise transactional email again.

The Real Problem With Email in Next.js Projects

Look, transactional email is one of those things everyone leaves for last. First the MVP, then onboarding, then payments... and email is "I'll fix it later."

The typical result is some combination of:

  • `nodemailer` with Gmail SMTP that gets blocked in production
  • SendGrid with a 2012 UI and unnecessary learning curve
  • HTML templates as JavaScript strings (the horror)
  • API routes that mix business logic with sending logic

In 2026, with everything we have available, this has no excuse anymore.

The Winner Stack: Resend + React Email + Server Actions

I've been using this combination across several projects and it's, without exaggeration, the first time email feels like a real part of the application and not a bolt-on patch.

Each piece has its reason:

Resend handles deliverability. DKIM/SPF authentication is configured by default, the API is clean and the Node.js SDK is excellent. In Spain and Europe, they also have EU servers, which simplifies GDPR compliance.

React Email lets you build email templates as React components. No HTML strings. No manually nested tables. Real components, with real props, testable.

Server Actions from Next.js 13+ eliminate the need to create API routes just to send an email. The logic lives where it should: close to the component that needs it.

The Pattern With Real Code

1. The Template as a React Component

```tsx // emails/welcome-email.tsx import { Html, Head, Body, Container, Text, Button, Preview, } from '@react-email/components';

interface WelcomeEmailProps { userName: string; confirmationUrl: string; }

export function WelcomeEmail({ userName, confirmationUrl }: WelcomeEmailProps) { return ( <Html> <Head /> <Preview>Confirm your account in less than a minute</Preview> <Body style={{ fontFamily: 'sans-serif', backgroundColor: '#f9fafb' }}> <Container style={{ maxWidth: '560px', margin: '0 auto', padding: '40px 20px' }}> <Text style={{ fontSize: '24px', fontWeight: 'bold' }}> Hey, {userName} 👋 </Text> <Text style={{ color: '#6b7280' }}> Thanks for signing up. Confirm your email to get started: </Text> <Button href={confirmationUrl} style={{ backgroundColor: '#0f172a', color: '#ffffff', padding: '12px 24px', borderRadius: '6px', textDecoration: 'none', }} > Confirm my account </Button> </Container> </Body> </Html> ); } ```

This is what changed my thinking: the template lives in your repo, with TypeScript, with typed props. If you change a field name, the compiler tells you. No more magic strings.

2. The Sending Logic as a Server Action

```ts // app/actions/send-welcome-email.ts 'use server';

import { Resend } from 'resend'; import { render } from '@react-email/render'; import { WelcomeEmail } from '@/emails/welcome-email';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function sendWelcomeEmail({ to, userName, confirmationUrl, }: { to: string; userName: string; confirmationUrl: string; }) { try { const html = render( <WelcomeEmail userName={userName} confirmationUrl={confirmationUrl} /> );

const { data, error } = await resend.emails.send({ from: 'Brian <hello@yourdomain.com>', to, subject: 'Confirm your account', html, });

if (error) { console.error('Error sending email:', error); return { success: false, error }; }

return { success: true, id: data?.id }; } catch (err) { console.error('Unexpected error:', err); return { success: false, error: err }; } } ```

3. Using It From Your Component or Route Handler

```ts // app/auth/register/route.ts import { sendWelcomeEmail } from '@/app/actions/send-welcome-email';

export async function POST(request: Request) { const { email, name } = await request.json();

// ... registration logic ...

const confirmationUrl = `https://yourapp.com/confirm?token=${token}`;

await sendWelcomeEmail({ to: email, userName: name, confirmationUrl, });

return Response.json({ success: true }); } ```

Clean. Typed. Predictable.

What This Solves That Other Approaches Don't

Local preview: React Email includes a preview server. You run `email dev` and see exactly how the email will look in the browser before sending anything. This alone saves hours.

Deliverability from day one: Resend manages domain reputation, DNS records and authentication. You don't need to be an expert in SPF, DKIM and DMARC for your emails to reach the inbox.

Real maintainability: When six months from now you need to redesign all your emails, you change the shared components. You're not hunting for HTML strings throughout the project.

GDPR and the European market: Resend has EU storage options. If you have users in Spain or any EU country, this isn't optional — it's a legal requirement under GDPR.

One Thing Most People Ignore

Welcome and password reset emails are the most tested. But in most projects, invoice emails, activity notifications or system alerts are never really tested until a customer complains.

With React Email you can write unit tests for your templates. You render the component with different props and verify the content is correct. The same thing you'd do with any UI component.

```ts // emails/__tests__/welcome-email.test.tsx import { render } from '@react-email/render'; import { WelcomeEmail } from '../welcome-email';

test('includes the user name', () => { const html = render( <WelcomeEmail userName="Ana García" confirmationUrl="https://test.com/confirm?token=abc" /> );

expect(html).toContain('Ana García'); expect(html).toContain('https://test.com/confirm?token=abc'); }); ```

The Takeaway

Transactional email is critical infrastructure. A user who doesn't receive their confirmation email is a user who won't convert. A user who doesn't get the password reset is a user who will abandon your app.

In 2026 there's no reason to improvise this anymore. Resend + React Email + Server Actions is the stack that gives you full control, good deliverability and a developer experience that doesn't make you hate email.

Two concrete steps to get started today:

1. Install `resend` and `@react-email/components` in your Next.js project. Resend's documentation has a quickstart that works in under ten minutes. 2. Migrate your most critical email first — the account confirmation or password reset. Once you see how it fits together, the rest comes naturally.

Keep building.

Brian Mena

Brian Mena

Software engineer building profitable digital products: SaaS, directories and AI agents. All from scratch, all in production.

LinkedIn