Resend Transactional Email Done Right in 2026
Production-ready patterns for Resend email API with real error handling, code examples, and why it beats traditional SMTP.
TL;DR
Resend (v3.x) is the fastest way to ship transactional emails without managing SMTP credentials. Built on Amazon SES, it handles bounces, complaints, and delivery tracking natively. This guide covers production patterns, common errors, and when to use it.
---
Why Resend Over Traditional SMTP?
Traditional transactional email stacks require:
Resend eliminates this. It's a purpose-built API (not a generic service bolted onto email). The pricing model is refreshingly simple: $0.20 per 1,000 emails after a generous free tier (verify in [official pricing](https://resend.com/pricing)).
The real win? Delivery infrastructure you don't maintain. Resend manages sender reputation, authentication (SPF, DKIM, DMARC setup), and provides a dashboard that shows what actually happened to your emails.
---
Getting Started: The Right Way
Installation (v3.x)
```bash npm install resend ```
Verify your installed version:
```bash npm list resend ```
Authentication
Create a .env.local file:
```env RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx ```
Grab your API key from [Resend dashboard](https://dashboard.resend.com). Never hardcode it.
---
Production Pattern #1: Basic Email with Error Handling
This is the pattern you'll use 80% of the time:
```typescript import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
interface SendEmailParams { to: string; subject: string; html: string; }
export async function sendTransactionalEmail({ to, subject, html, }: SendEmailParams) { try { const data = await resend.emails.send({ from: 'notifications@yourdomain.com', to, subject, html, });
if (data.error) {
console.error('Resend API error:', data.error);
throw new Error(Email send failed: ${data.error.message});
}
console.log(Email sent to ${to}. ID: ${data.data?.id});
return data.data;
} catch (error) {
console.error('Unexpected error sending email:', error);
throw error;
}
}
```
Real console error you'll see:
``` Resend API error: { message: 'Invalid "from" email address. Please set a verified sender identity first.', code: 'INVALID_FROM_ADDRESS' } ```
Fix: Add your domain to Resend dashboard and verify DNS records (SPF, DKIM, DMARC).
---
Production Pattern #2: React Email Templates
Resend's killer feature is [React Email](https://react.email) integration. Ship components instead of HTML strings:
```typescript import { render } from '@react-email/render'; import { Resend } from 'resend';
interface WelcomeEmailProps { userName: string; confirmLink: string; }
function WelcomeEmail({ userName, confirmLink }: WelcomeEmailProps) { return ( <div style={{ fontFamily: 'Arial, sans-serif' }}> <h1>Welcome, {userName}!</h1> <p>Confirm your email to get started:</p> <a href={confirmLink} style={{ background: '#000', color: '#fff', padding: '12px 20px', textDecoration: 'none', borderRadius: '4px', display: 'inline-block', }} > Confirm Email </a> </div> ); }
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendWelcomeEmail( email: string, userName: string, confirmLink: string ) { const emailHtml = render( WelcomeEmail({ userName, confirmLink }) );
const result = await resend.emails.send({
from: 'welcome@yourdomain.com',
to: email,
subject: Welcome ${userName}!,
html: emailHtml,
});
return result; } ```
Real console error you'll encounter:
``` Error: Component is not a valid render result. Expected a React component, but got: undefined
This happens when you forget to export the component or pass it incorrectly to render(). ```
---
Production Pattern #3: Batch Emails with Rate Limiting
When sending 1000+ emails, respect Resend's limits:
```typescript import pQueue from 'p-queue';
const queue = new pQueue({ concurrency: 5, interval: 1000, intervalCap: 10 });
export async function sendBatchEmails(
recipients: Array<{ email: string; name: string }>
) {
const results = await Promise.allSettled(
recipients.map((recipient) =>
queue.add(() =>
resend.emails.send({
from: 'batch@yourdomain.com',
to: recipient.email,
subject: Hello ${recipient.name},
html: <p>This is email ${recipient.email}</p>,
})
)
)
);
const successful = results.filter((r) => r.status === 'fulfilled').length; const failed = results.filter((r) => r.status === 'rejected').length;
console.log(Batch sent: ${successful} successful, ${failed} failed);
return { successful, failed };
}
```
Real console error you'll hit:
``` Error: Rate limit exceeded Status: 429 Retry-After: 60
This means you're sending too fast. Implement exponential backoff. ```
---
Handling Bounce & Complaint Webhooks
Emails bounce. Set up webhooks to handle them:
```typescript // In your API route (e.g., /api/resend-webhook)
export async function POST(req: Request) { const body = await req.json();
if (body.type === 'email.bounced') {
const { email } = body.data;
console.log(Email bounced: ${email});
// Mark as undeliverable in your database
await db.user.update(
{ email },
{ emailStatus: 'bounced', bounceReason: body.data.bounce_type }
);
}
if (body.type === 'email.complained') {
const { email } = body.data;
console.log(User complained: ${email});
// Unsubscribe immediately
await db.user.update({ email }, { unsubscribed: true });
}
return Response.json({ ok: true }); } ```
Add your webhook URL in [Resend dashboard](https://dashboard.resend.com/webhooks).
---
When Resend Is the Right Choice
✅ Transactional email only (password resets, confirmations, receipts) ✅ Fewer than 100K emails/month ✅ You want reliable deliverability out of the box ✅ Sending from a Next.js or Node.js app
❌ High-volume marketing campaigns (use SendGrid or Mailgun) ❌ Extreme scale (>10M emails/month; negotiate SES directly) ❌ Legacy SMTP dependency required
---
Related Topics
---
Verification Notes
For API version details and pricing updates, always verify in [official Resend documentation](https://resend.com/docs). Prices and features mentioned reflect early 2026; request latest in comments if changed.
---
What am I missing?
Have you shipped Resend in production? What broke? What saved you time? Leave your real-world patterns, error messages you've hit, and edge cases in the comments. If you found better solutions for batch sending, webhook handling, or templating—I want to know.