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:
- A Resend account with an API key
- A verified sending domain in Resend (or use their test address for development)
- A Next.js project (App Router or Pages Router — both covered)
- Node.js 18+
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.tsximport * 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 succeededreturn 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?
- Check the Resend dashboard logs first — the delivery status is shown per email
- Check your spam folder
- Verify your sending domain is fully verified (DNS records propagated)
- 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:
// ✅ Correctreact: WelcomeEmail({ username, dashboardUrl }),// orreact: <WelcomeEmail username={username} dashboardUrl={dashboardUrl} />,
// ❌ Wrongreact: 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.tsimport { 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.
Related Resources
- Template examples: 4 ready-to-use React email templates with code
- Monorepo setup: Setting up React Email in a monorepo
- SaaS onboarding flow: Best React email templates for SaaS onboarding
- Full SaaS email guide: 12 React email templates every SaaS app needs
