Blog/Send React Email with Resend in Next.js (Complete 2026 Guide)
Guides

Send React Email with Resend in Next.js (Complete 2026 Guide)

Complete guide to sending React Email templates with Resend in Next.js. Covers env setup, App Router & Pages Router, local preview, multiple templates, error handling, and TypeScript tips.

Soufiane E.
Soufiane E.
·
·
9 min read
·
Updated March 20, 2026
Send React Email with Resend in Next.js (Complete 2026 Guide)

Send React Email with Resend in Next.js (Complete 2026 Guide)

If you're building a SaaS app, you need transactional emails: welcome messages, OTP codes, password resets, payment receipts. Instead of hand-writing HTML tables, use React Email to build your templates and Resend to deliver them reliably.

This guide covers everything from first install to sending production emails — including App Router and Pages Router examples, environment variables, local preview, multiple template types, error handling, and TypeScript tips.


Prerequisites

Before starting, you need:


Step 1: Install Dependencies

Install the Resend SDK:

npm install resend

Install React Email components for building your templates:

npm install @react-email/components react react-dom

Step 2: Configure Environment Variables

Create or update your .env.local file:

RESEND_API_KEY=re_your_api_key_here

Important: Never expose your API key in client-side code. Always access it only from server-side routes and API handlers. In Next.js, any environment variable without the NEXT_PUBLIC_ prefix is automatically server-only.

If you deploy to Vercel, add the same variable in your project settings under Settings → Environment Variables. On Railway, Fly.io, or similar platforms, set it through their dashboard or CLI.


Step 3: Create a React Email Template

Create a dedicated directory for your email templates:

mkdir -p emails

Add your first template at emails/welcome-email.tsx:

import * as React from 'react';
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Section,
Text,
} from '@react-email/components';
interface WelcomeEmailProps {
username: string;
dashboardUrl: string;
}
export function WelcomeEmail({ username, dashboardUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome — your account is ready!</Preview>
<Body style={{ fontFamily: 'sans-serif', backgroundColor: '#f4f4f5' }}>
<Container style={{ margin: '0 auto', padding: '40px 20px', maxWidth: '560px' }}>
<Heading style={{ fontSize: '24px', fontWeight: 'bold' }}>
Welcome, {username}!
</Heading>
<Text style={{ fontSize: '16px', lineHeight: '26px' }}>
Thanks for signing up. Your account is ready to use.
</Text>
<Section>
<Button
href={dashboardUrl}
style={{
backgroundColor: '#000',
color: '#fff',
padding: '12px 24px',
borderRadius: '6px',
textDecoration: 'none',
fontSize: '16px',
}}
>
Go to Dashboard
</Button>
</Section>
<Text style={{ fontSize: '14px', color: '#6b7280', marginTop: '24px' }}>
If you have questions, reply to this email or check our docs.
</Text>
</Container>
</Body>
</Html>
);
}
WelcomeEmail.PreviewProps = {
username: 'Alex',
dashboardUrl: 'https://example.com/dashboard',
} as WelcomeEmailProps;
export default WelcomeEmail;

The PreviewProps field lets the React Email dev server render your template with realistic data during local development.


Step 4: Preview Your Templates Locally

Before sending any real email, use the React Email dev server to see exactly how your templates render:

npx react-email dev --dir emails

Open http://localhost:3000 to preview all your templates. You can switch between templates, toggle between desktop and mobile views, and catch layout issues before they reach users.

This is especially important because different email clients (Gmail, Outlook, Apple Mail) render HTML very differently. Catching visual bugs here saves a lot of debugging later.



Step 5: Create the API Route

App Router (app/api/send/route.ts)

import { Resend } from 'resend';
import { WelcomeEmail } from '../../../emails/welcome-email';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(request: Request) {
const { username, email } = await request.json();
const { data, error } = await resend.emails.send({
from: 'Your App <hello@yourdomain.com>',
to: [email],
subject: `Welcome to Your App, ${username}!`,
react: WelcomeEmail({
username,
dashboardUrl: 'https://yourdomain.com/dashboard',
}),
});
if (error) {
return Response.json({ error: error.message }, { status: 500 });
}
return Response.json({ id: data?.id }, { status: 200 });
}

Pages Router (pages/api/send.ts)

import type { NextApiRequest, NextApiResponse } from 'next';
import { Resend } from 'resend';
import { WelcomeEmail } from '../../emails/welcome-email';
const resend = new Resend(process.env.RESEND_API_KEY);
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { username, email } = req.body;
const { data, error } = await resend.emails.send({
from: 'Your App <hello@yourdomain.com>',
to: [email],
subject: `Welcome to Your App, ${username}!`,
react: WelcomeEmail({
username,
dashboardUrl: 'https://yourdomain.com/dashboard',
}),
});
if (error) {
return res.status(500).json({ error: error.message });
}
return res.status(200).json({ id: data?.id });
}

Step 6: Test the Send

Call your API route from anywhere in your app:

const response = await fetch('/api/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: 'Alex',
email: 'alex@example.com',
}),
});
const result = await response.json();
console.log(result); // { id: 're_...' }

For local development without sending real emails, set your to field to delivered@resend.dev — Resend processes the email without delivering it, and it shows up in your dashboard logs.


Sending Multiple Email Types

A SaaS app needs more than a welcome email. Here is how to add an OTP template and send it alongside your welcome email:

// emails/otp-email.tsx
import * as React from 'react';
import {
Body,
Container,
Head,
Html,
Preview,
Section,
Text,
} from '@react-email/components';
interface OTPEmailProps {
code: string;
expiresIn?: string;
}
export function OTPEmail({ code, expiresIn = '10 minutes' }: OTPEmailProps) {
return (
<Html>
<Head />
<Preview>Your verification code: {code}</Preview>
<Body style={{ fontFamily: 'sans-serif' }}>
<Container style={{ margin: '0 auto', padding: '40px 20px', maxWidth: '560px' }}>
<Text style={{ fontSize: '16px' }}>Your one-time verification code:</Text>
<Section
style={{
background: '#f4f4f5',
padding: '20px',
borderRadius: '8px',
textAlign: 'center',
}}
>
<Text
style={{
fontSize: '36px',
fontWeight: 'bold',
letterSpacing: '10px',
margin: 0,
}}
>
{code}
</Text>
</Section>
<Text style={{ color: '#6b7280', fontSize: '14px' }}>
This code expires in {expiresIn}. Do not share it with anyone.
</Text>
</Container>
</Body>
</Html>
);
}
OTPEmail.PreviewProps = {
code: '482915',
expiresIn: '10 minutes',
} as OTPEmailProps;
export default OTPEmail;

Then in your API route, pick the right template based on type:

import { WelcomeEmail } from '../../../emails/welcome-email';
import { OTPEmail } from '../../../emails/otp-email';
export async function POST(request: Request) {
const { type, email, ...props } = await request.json();
const templates = {
welcome: <WelcomeEmail username={props.username} dashboardUrl={props.dashboardUrl} />,
otp: <OTPEmail code={props.code} />,
};
const template = templates[type as keyof typeof templates];
if (!template) {
return Response.json({ error: 'Unknown email type' }, { status: 400 });
}
const { data, error } = await resend.emails.send({
from: 'Your App <hello@yourdomain.com>',
to: [email],
subject: type === 'otp' ? 'Your verification code' : 'Welcome!',
react: template,
});
if (error) {
return Response.json({ error: error.message }, { status: 500 });
}
return Response.json({ id: data?.id });
}

Error Handling

Resend returns a typed error object. Always check for it before assuming success:

const { data, error } = await resend.emails.send({ ... });
if (error) {
console.error('Resend error:', error.name, error.message);
// Log to your error tracker (Sentry, etc.)
return Response.json({ error: error.message }, { status: 500 });
}
// Only reach here if send succeeded
return Response.json({ id: data?.id });

Common errors and fixes:

| Error | Cause | Fix | |-------|-------|-----| | invalid_api_key | Wrong or missing API key | Check .env.local and redeploy | | not_allowed | Sending domain not verified | Verify domain in Resend dashboard | | validation_error | Invalid from format | Use Name <email@verified-domain.com> | | missing_required_field | Missing to, from, or subject | Check all required fields | | rate_limit_exceeded | Too many requests | Add retry logic with exponential backoff |


Troubleshooting

Email not arriving?

  1. Check the Resend dashboard logs first — the delivery status is shown per email
  2. Check your spam folder
  3. Verify your sending domain is fully verified (DNS records propagated)
  4. Confirm you're not using delivered@resend.dev (test address) in production

Template looks broken in Outlook? Outlook has poor CSS support. Avoid flexbox, grid, and shorthand CSS properties. Use the Row, Column, and Section primitives from @react-email/components — they generate table-based layouts that work across all major clients including Outlook 2016–2024.

RESEND_API_KEY is undefined in production? Environment variables in .env.local do not deploy automatically. Add them manually in your hosting provider's environment settings — Vercel dashboard, Railway project settings, etc.

Getting TypeError: react is not a valid element? Make sure you pass the JSX element, not the function itself:

// ✅ Correct
react: WelcomeEmail({ username, dashboardUrl }),
// or
react: <WelcomeEmail username={username} dashboardUrl={dashboardUrl} />,
// ❌ Wrong
react: WelcomeEmail,

TypeScript: Reusable Email Function

Type your email-sending logic as a reusable utility so you don't repeat the Resend setup across routes:

// lib/email.ts
import { Resend } from 'resend';
import type { CreateEmailOptions } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendEmail(options: CreateEmailOptions) {
const { data, error } = await resend.emails.send(options);
if (error) {
throw new Error(`Email send failed: ${error.message}`);
}
return data;
}

Usage:

import { sendEmail } from '@/lib/email';
import { WelcomeEmail } from '@/emails/welcome-email';
await sendEmail({
from: 'Your App <hello@yourdomain.com>',
to: [user.email],
subject: `Welcome, ${user.name}!`,
react: <WelcomeEmail username={user.name} dashboardUrl="/dashboard" />,
});

Organizing Multiple Templates

As your app grows, you'll need more email types: password resets, payment receipts, trial reminders, security alerts, and more. A consistent folder structure helps:

emails/
├── welcome-email.tsx # After signup
├── otp-email.tsx # OTP / magic code
├── magic-link.tsx # Passwordless login
├── password-reset.tsx # Password recovery
├── payment-receipt.tsx # After payment
├── trial-started.tsx # Trial activation
├── trial-ending.tsx # Trial expiry warning
└── security-alert.tsx # Suspicious login, etc.

For a monorepo setup where multiple apps share the same email templates, see our guide on setting up React Email in a monorepo.

If you don't want to write and test all these templates from scratch, our React Email Templates bundle includes 12 production-ready templates for the most common SaaS email flows — all tested on Gmail, Outlook, Apple Mail, and mobile clients.