You can't iterate on email design by deploying to production and checking your inbox. You need previews.
But most React Email preview setups stop at npm run dev— which works fine solo, but breaks down the moment you need teammates to review, QA to test across clients, or a safe way to preview templates in staging without sending real emails.
Pattern 1: Local Preview Server (React Email Dev Mode)
Start here. React Email ships with a built-in preview server that renders all templates with hot reload. Perfect for solo development.
# Install React Email
npm install react-email @react-email/components
# Add to package.json scripts
{
"email:dev": "email dev"
}
# Start preview server
npm run email:devThis starts a preview server at localhost:3000 with a sidebar listing all your email templates. Click any template to see it rendered with preview data.
preview-data.ts file that exports realistic data fixtures for each template. Makes iteration way faster than hardcoding props in each file.File Structure That Works
emails/
├── templates/
│ ├── welcome.tsx
│ ├── password-reset.tsx
│ └── invoice.tsx
├── preview-data/
│ ├── welcome.ts
│ ├── password-reset.ts
│ └── invoice.ts
└── components/
└── base-layout.tsxEach template gets a matching preview data file. React Email automatically picks up the exported default.
export default {
userName: "Alex Morgan",
ctaUrl: "https://app.yourproduct.com/onboarding",
trialDays: 14,
supportEmail: "help@yourproduct.com",
};Pattern 2: Team Preview Endpoint (for Design Review)
Local previews don't help teammates who aren't running the dev server. Add a /api/email-preview route that renders templates on-demand with query params.
import { render } from "@react-email/render";
import { NextRequest, NextResponse } from "next/server";
import WelcomeEmail from "@/emails/templates/welcome";
import PasswordResetEmail from "@/emails/templates/password-reset";
import previewData from "@/emails/preview-data";
// Only allow in non-production
if (process.env.NODE_ENV === "production") {
throw new Error("Preview route should not exist in production");
}
const TEMPLATES = {
welcome: WelcomeEmail,
"password-reset": PasswordResetEmail,
// Add more...
};
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const template = searchParams.get("template");
const variant = searchParams.get("variant") || "default";
if (!template || !TEMPLATES[template as keyof typeof TEMPLATES]) {
return NextResponse.json(
{ error: "Invalid template" },
{ status: 400 }
);
}
const TemplateComponent = TEMPLATES[template as keyof typeof TEMPLATES];
const props = previewData[template]?.[variant] || previewData[template];
const html = render(<TemplateComponent {...props} />);
return new NextResponse(html, {
headers: { "Content-Type": "text/html" },
});
}Now teammates can preview templates at:/api/email-preview?template=welcome
Pattern 3: Staging Preview Gallery (QA-Friendly)
Build a dedicated preview gallery page that lists all templates with variants. Great for QA teams testing across different email clients.
import { getAllTemplates } from "@/lib/email-registry";
export default function EmailPreviewPage() {
const templates = getAllTemplates();
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-6">Email Preview Gallery</h1>
<div className="grid gap-4">
{templates.map((template) => (
<div key={template.slug} className="border rounded-lg p-4">
<h2 className="font-semibold">{template.name}</h2>
<p className="text-sm text-gray-600 mb-3">{template.description}</p>
<div className="flex gap-2">
<a
href={`/api/email-preview?template=${template.slug}`}
target="_blank"
className="text-blue-600 hover:underline text-sm"
>
Preview Default
</a>
{template.variants?.map((variant) => (
<a
key={variant}
href={`/api/email-preview?template=${template.slug}&variant=${variant}`}
target="_blank"
className="text-blue-600 hover:underline text-sm"
>
Preview {variant}
</a>
))}
</div>
</div>
))}
</div>
</div>
);
}Add a simple template registry to make this work:
export const EMAIL_TEMPLATES = [
{
slug: "welcome",
name: "Welcome Email",
description: "Sent after user signs up",
variants: ["with-trial", "immediate-activation"],
},
{
slug: "password-reset",
name: "Password Reset",
description: "Security email for password resets",
variants: ["standard", "suspicious-activity"],
},
// Add more...
];
export function getAllTemplates() {
return EMAIL_TEMPLATES;
}Pattern 4: CI-Deployed Previews (for PR Reviews)
Deploy preview environments for every pull request. Lets designers and stakeholders review email changes without pulling code locally.
Works great with:
- Vercel: Automatic preview deployments for every PR
- Netlify: Deploy previews with unique URLs
- GitHub Actions: Custom preview deployment workflow
name: Deploy Email Previews
on:
pull_request:
paths:
- 'emails/**'
- 'components/**'
jobs:
deploy-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build email previews
run: npm run email:build
- name: Deploy to preview environment
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
npx vercel --token=$VERCEL_TOKEN --yes
- name: Comment preview URL on PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '📧 Email preview deployed: https://preview-${{ github.event.pull_request.number }}.yourapp.com/email-preview'
})Pattern 5: Production-Safe Preview Route (Read-Only Admin)
Sometimes you need to preview emails in production - to debug issues, show stakeholders what was sent, or test with real data. But you can't just expose a public preview endpoint.
Add an admin-only, read-only preview route with proper authentication:
import { render } from "@react-email/render";
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export async function GET(request: NextRequest) {
// 1. Require authentication
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// 2. Rate limit
const ip = request.headers.get("x-forwarded-for") || "unknown";
const rateLimitOk = await checkRateLimit(ip, { max: 10, window: 60 });
if (!rateLimitOk) {
return NextResponse.json(
{ error: "Rate limit exceeded" },
{ status: 429 }
);
}
// 3. Render template with sanitized props
const searchParams = request.nextUrl.searchParams;
const template = searchParams.get("template");
const dataId = searchParams.get("dataId");
// Fetch safe preview data (never expose sensitive user data)
const safeProps = await getPreviewData(template, dataId);
const html = render(getTemplateComponent(template, safeProps));
return new NextResponse(html, {
headers: {
"Content-Type": "text/html",
"X-Robots-Tag": "noindex, nofollow",
"Cache-Control": "no-store, max-age=0",
},
});
}Security checklist for production previews:
- Require admin authentication
- Rate limit aggressively
- Never expose real user data (use sanitized fixtures)
- Add noindex headers
- Log all access for audit trail
- Consider IP allowlisting for extra paranoia
Bonus: Cross-Client Preview Testing
Previews in a browser aren't enough. Gmail, Outlook, and Apple Mail render HTML differently. Use these tools to test real client rendering:
- Email on Acid: Automated cross-client screenshots
- Litmus: Preview + spam testing
- Mailtrap: Safe inbox for staging email testing
- Playwright: Automated visual regression tests
import { test, expect } from "@playwright/test";
test("Welcome email renders correctly", async ({ page }) => {
await page.goto("http://localhost:3000/api/email-preview?template=welcome");
// Wait for email to render
await page.waitForSelector("body");
// Take screenshot for visual regression
await expect(page).toHaveScreenshot("welcome-email.png", {
fullPage: true,
});
// Verify critical elements
await expect(page.locator("h1")).toContainText("Welcome");
await expect(page.locator('a[href*="onboarding"]')).toBeVisible();
});Which Patterns to Use?
Solo developer / early startup:
- Pattern 1 (local preview)
- Pattern 2 (team preview API)
Small team (2-10 people):
- Pattern 1 (local preview)
- Pattern 3 (staging gallery)
- Pattern 4 (CI previews for PRs)
Production SaaS with compliance requirements:
- All patterns
- Pattern 5 (production admin previews)
- Automated cross-client testing
- Audit logging for preview access
Common Mistakes
1) Exposing preview routes in production
Don't leave /api/email-preview accessible in production without auth. Attackers can use it to:
- Enumerate email templates
- Discover internal URLs
- Test injection attacks
Fix: Add environment checks or move preview routes to admin-only paths.
2) Using production data in preview fixtures
Never pull real user data into preview environments. Use sanitized fixtures or synthetic data.
// ❌ Bad: Exposes real user data
const props = await db.user.findFirst();
// ✅ Good: Synthetic preview data
const props = {
userName: "Alex Morgan (Test User)",
email: "preview@example.com",
ctaUrl: "https://staging.yourapp.com/test",
};3) No variant support
Most templates have edge cases: long names, missing data, different states. Preview all variants, not just the happy path.
export default {
default: {
invoiceNumber: "INV-2024-001",
lineItems: [
{ name: "Pro Plan", amount: 49.00 },
{ name: "Additional seats (2)", amount: 20.00 },
],
total: 69.00,
},
singleItem: {
invoiceNumber: "INV-2024-002",
lineItems: [
{ name: "Starter Plan", amount: 19.00 },
],
total: 19.00,
},
longNames: {
invoiceNumber: "INV-2024-003",
lineItems: [
{
name: "Enterprise Plan with Additional Features and Custom Integration Support",
amount: 499.00
},
],
total: 499.00,
},
};Implementation Checklist
- Set up React Email dev server for local iteration
- Create preview data fixtures for all templates
- Add team preview API route (dev/staging only)
- Build preview gallery page for QA
- Set up CI preview deployments for PRs
- Add production admin preview route (if needed)
- Test across Gmail, Outlook, Apple Mail
- Add visual regression tests
Good preview environments turn email iteration from "deploy and pray" into a confident, collaborative workflow. Start with local previews, add team/staging patterns as you scale, and keep production previews locked down.