Resend: Transactional Email Done Right in 2026

Production-ready patterns for Resend v3+. Real errors, exact setup, and patterns that actually scale for indie hackers.

TL;DR

Resend handles transactional email without the DevOps nightmare. We'll cover v3+ setup, production patterns, common errors, and why it beats the alternatives for indie projects.

---

Why Resend Matters

Transactional email is boring infrastructure that shouldn't consume your engineering time. Most teams either:

1. Use AWS SES and spend 3 weeks debugging deliverability 2. Use SendGrid and get a $300/month bill for 10k emails 3. Use Gmail SMTP and watch it rate-limit at 2am

Resend eliminates this. It's built for developers shipping fast. The API is minimal, the docs are honest, and the pricing doesn't punish scale.

Verify current pricing and limits in [official Resend docs](https://resend.com/docs) - but the general structure: $0 setup, pay-per-email at volume.

---

Setup (v3.x)

```bash npm install resend@latest

Or verify version: npm view resend versions

```

Grab your API key from [Resend dashboard](https://resend.com/api-keys). Create .env.local:

```env RESEND_API_KEY=re_xxxxxxxxxxxxx ```

---

Production Pattern: Typed Email Templates

Here's a pattern that scales without becoming spaghetti:

```typescript // lib/email/templates.ts import { Resend } from 'resend';

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

// Type-safe email definitions interface EmailTemplate<T> { subject: string; template: (data: T) => string; from?: string; }

const templates = { welcome: { subject: 'Welcome to our platform', template: (data: { name: string; confirmUrl: string }) => ` <h1>Welcome, ${data.name}!</h1> <p><a href="${data.confirmUrl}">Confirm your email</a></p> `, from: 'onboarding@example.com', } as EmailTemplate<{ name: string; confirmUrl: string }>,

passwordReset: { subject: 'Reset your password', template: (data: { resetUrl: string; expiresIn: string }) => ` <p><a href="${data.resetUrl}">Reset password</a></p> <p>Link expires in ${data.expiresIn}.</p> `, from: 'security@example.com', } as EmailTemplate<{ resetUrl: string; expiresIn: string }>, };

export default templates; ```

Now create a sender wrapper with retry logic:

```typescript // lib/email/send.ts import { Resend } from 'resend'; import templates from './templates';

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

interface SendEmailOptions { templateKey: keyof typeof templates; to: string | string[]; data: Record<string, unknown>; replyTo?: string; }

export async function sendEmail({ templateKey, to, data, replyTo, }: SendEmailOptions) { const template = templates[templateKey];

if (!template) { throw new Error(Template "${templateKey}" not found); }

const toArray = Array.isArray(to) ? to : [to]; const retries = 3; let lastError: Error | null = null;

for (let attempt = 1; attempt <= retries; attempt++) { try { const response = await resend.emails.send({ from: template.from || 'noreply@example.com', to: toArray, subject: template.subject, html: template.template(data as never), replyTo, });

if (response.error) { throw new Error(Resend API error: ${response.error.message}); }

console.log([Email] Sent "${templateKey}" to ${toArray.join(',')}); return response.data; } catch (error) { lastError = error as Error; const isLastAttempt = attempt === retries;

if (isLastAttempt) { console.error( [Email] Failed after ${retries} attempts:, lastError.message ); throw lastError; }

// Exponential backoff: 1s, 2s, 4s const delayMs = Math.pow(2, attempt - 1) * 1000; await new Promise((resolve) => setTimeout(resolve, delayMs)); } }

throw lastError || new Error('Email send failed'); } ```

Usage in your route or action:

```typescript // app/api/auth/signup/route.ts import { sendEmail } from '@/lib/email/send';

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

// ... validation and user creation ...

try { await sendEmail({ templateKey: 'welcome', to: email, data: { name, confirmUrl: ${process.env.NEXT_PUBLIC_BASE_URL}/confirm?token=xyz, }, }); } catch (error) { console.error('Welcome email failed:', error); // Don't fail signup - queue for retry or alert ops }

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

---

Real Errors You'll See

Error 1: Missing Domain Verification

``` Error: Invalid 'from' address. Please add and verify the domain in Resend dashboard. ```

Fix: Go to Resend dashboard → Domains → Add domain → Verify DNS records. Takes 5-10 minutes.

Error 2: Invalid Email Addresses

``` Error: Invalid email address format for recipient ```

Fix: Sanitize emails before sending:

```typescript function isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }

if (!isValidEmail(to)) { throw new Error(Invalid email: ${to}); } ```

Error 3: Rate Limiting (v3.x behavior)

``` Error: Too many requests. Please retry after 60 seconds. ```

Fix: Implement queue-based sending for bulk operations, or use Resend's batch endpoint. For individual transactional emails, this is rare. If you hit it, you have bigger growth problems—good problems.

---

Production Checklist

  • [ ] Domain verified in Resend dashboard
  • [ ] API key in .env.local (never .env)
  • [ ] Retry logic with exponential backoff
  • [ ] Email validation before sending
  • [ ] Unsubscribe headers for compliance
  • [ ] Log all send attempts (include response.id for debugging)
  • [ ] Monitor delivery via Resend dashboard or webhook
  • [ ] Test with [Resend sandbox](https://resend.com/docs/concepts/testing) during dev
  • ---

    Alternatives (Why Resend Wins)

    | Service | Setup Time | Price | Developer Experience | |---------|-----------|-------|----------------------| | Resend | 15 min | Pay-per-email | 🟢 Excellent | | SendGrid | 30 min | $20+ | 🟡 Good but heavier | | AWS SES | 2 hours | Cheap but complex | 🔴 Painful | | Mailgun | 45 min | $20+ | 🟡 Good for power users |

    For indie hackers: Resend's simplicity + reliability combo is unbeatable.

    ---

    Resources

  • [Resend Official Docs](https://resend.com/docs)
  • [API Reference](https://resend.com/docs/api-reference/emails/send)
  • [Email Best Practices](https://resend.com/docs/concepts/introduction)
  • Related: [Error Handling Patterns](/?guide=error-handling-nodejs)
  • Related: [API Rate Limiting Strategies](/?guide=rate-limiting-guide)
  • ---

    What am I missing?

    Please comment with:

  • Your Resend gotchas or deployment patterns
  • Batch email strategies that worked
  • Webhook integration examples
  • Cost comparisons you've measured
  • React Email component libraries you use with Resend
  • Accuracies matter here—if pricing or version details changed, flag it and I'll update. This is indie developer infrastructure; we're all shipping together.

    🔥 0d
    LIVE
    ↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising •↑ Resend loved by devs •↓ PlanetScale rage spiking •⚠ Vercel pricing complaints •↑ Railway gaining fast •↑ Supabase happiness rising
    DEVELOPER PAIN RADAR // Loading...

    Developers complain.
    Opportunities appear.

    We track what developers are struggling with today — and what opportunities that creates.

    —
    guides today
    —
    avg happiness
    🔥 Pain
    📖 Guides
    🔭 Explore
    👤 Mine
    🔥 Pain Radar — rage scores today
    ↗ share
    💡 Opportunity Feed — pain = market gap
    📈 Tool Momentum
    all scores →
    📖 Latest Guide
    all guides →
    📖 All Guides
    📊 Tool Scores
    + Submit
    📰 Hacker News
    ➕ Submit a Tool
    ← back