Resend - Nowoczesne Email API dla Developerów
Czym jest Resend?
Resend to nowoczesna platforma do wysyłania emaili, stworzona przez twórców React Email. W przeciwieństwie do tradycyjnych rozwiązań SMTP, Resend oferuje czyste, developer-friendly API z natywnym wsparciem dla TypeScript i React. To idealne narzędzie dla aplikacji Next.js, które potrzebują niezawodnego systemu wysyłki emaili transakcyjnych.
Dlaczego Resend?
Problemy z tradycyjnymi rozwiązaniami
Wysyłanie emaili zawsze było problematyczne dla developerów:
- SendGrid/Mailgun: Skomplikowane dashboardy, słabe DX
- Nodemailer: Wymaga własnego serwera SMTP, problemy z dostarczalnością
- AWS SES: Trudna konfiguracja, surowe limity
- SMTP bezpośrednio: Problemy z reputacją IP, blacklisty
Zalety Resend
- React Email - Twórz szablony emaili jako komponenty React
- Type-safe API - Pełne wsparcie TypeScript
- Wysoka dostarczalność - Własna infrastruktura z monitoringiem
- Proste API - Intuicyjne, bez zbędnej konfiguracji
- Webhooks - Śledzenie statusu emaili w czasie rzeczywistym
- Domains verification - Łatwa konfiguracja własnych domen
Instalacja i konfiguracja
Instalacja pakietów
# Główny pakiet Resend
npm install resend
# React Email dla szablonów (opcjonalnie, ale zalecane)
npm install @react-email/components
# Dla podglądu szablonów lokalnie
npm install react-email --save-devKonfiguracja API Key
Utwórz konto na resend.com i wygeneruj API key:
// lib/resend.ts
import { Resend } from 'resend'
if (!process.env.RESEND_API_KEY) {
throw new Error('Missing RESEND_API_KEY environment variable')
}
export const resend = new Resend(process.env.RESEND_API_KEY)# .env.local
RESEND_API_KEY=re_xxxxxxxxxxxxxPodstawowe wysyłanie emaili
Prosty email tekstowy
import { resend } from '@/lib/resend'
const { data, error } = await resend.emails.send({
from: 'hello@yourdomain.com',
to: 'user@example.com',
subject: 'Witaj w naszej aplikacji!',
text: 'Dziękujemy za rejestrację. Twoje konto jest już aktywne.',
})
if (error) {
console.error('Failed to send email:', error)
return
}
console.log('Email sent:', data.id)Email z HTML
const { data, error } = await resend.emails.send({
from: 'notifications@yourdomain.com',
to: ['user1@example.com', 'user2@example.com'],
subject: 'Nowe zamówienie #12345',
html: `
<h1>Potwierdzenie zamówienia</h1>
<p>Dziękujemy za złożenie zamówienia!</p>
<p>Numer zamówienia: <strong>#12345</strong></p>
<a href="https://example.com/orders/12345">Zobacz szczegóły</a>
`,
})Opcje zaawansowane
const { data, error } = await resend.emails.send({
from: 'Jan Kowalski <jan@yourdomain.com>',
to: 'user@example.com',
cc: ['manager@company.com'],
bcc: ['archive@company.com'],
reply_to: 'support@yourdomain.com',
subject: 'Ważna wiadomość',
html: '<p>Treść wiadomości</p>',
// Załączniki
attachments: [
{
filename: 'raport.pdf',
content: pdfBuffer, // Buffer lub base64 string
},
],
// Tagi do śledzenia
tags: [
{ name: 'category', value: 'order_confirmation' },
{ name: 'user_id', value: '12345' },
],
// Planowane wysłanie
scheduled_at: '2024-12-25T09:00:00Z',
// Nagłówki
headers: {
'X-Entity-Ref-ID': 'order-12345',
},
})React Email - Szablony jako komponenty
Tworzenie szablonu
React Email pozwala tworzyć szablony emaili jako komponenty React:
// emails/welcome.tsx
import {
Html,
Head,
Body,
Container,
Section,
Text,
Button,
Img,
Link,
Preview,
Hr,
} from '@react-email/components'
interface WelcomeEmailProps {
username: string
verificationUrl: string
}
export default function WelcomeEmail({
username,
verificationUrl,
}: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Witaj w CodeWorlds, {username}!</Preview>
<Body style={main}>
<Container style={container}>
<Img
src="https://yourdomain.com/logo.png"
width={150}
height={50}
alt="CodeWorlds"
/>
<Section style={section}>
<Text style={heading}>Witaj, {username}!</Text>
<Text style={text}>
Dziękujemy za dołączenie do CodeWorlds. Twoja przygoda z
programowaniem właśnie się rozpoczyna!
</Text>
<Button style={button} href={verificationUrl}>
Aktywuj konto
</Button>
<Text style={text}>
Lub skopiuj ten link do przeglądarki:
</Text>
<Link href={verificationUrl} style={link}>
{verificationUrl}
</Link>
</Section>
<Hr style={hr} />
<Section style={footer}>
<Text style={footerText}>
© 2024 CodeWorlds. Wszystkie prawa zastrzeżone.
</Text>
<Link href="https://yourdomain.com/unsubscribe" style={footerLink}>
Wypisz się z newslettera
</Link>
</Section>
</Container>
</Body>
</Html>
)
}
// Style inline (wymagane dla emaili)
const main = {
backgroundColor: '#f6f9fc',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}
const container = {
backgroundColor: '#ffffff',
margin: '0 auto',
padding: '20px 0 48px',
marginBottom: '64px',
}
const section = {
padding: '0 48px',
}
const heading = {
fontSize: '24px',
fontWeight: 'bold',
marginBottom: '16px',
}
const text = {
fontSize: '16px',
lineHeight: '26px',
color: '#333',
}
const button = {
backgroundColor: '#5046e5',
borderRadius: '6px',
color: '#fff',
fontSize: '16px',
fontWeight: 'bold',
textDecoration: 'none',
textAlign: 'center' as const,
display: 'block',
padding: '12px 24px',
margin: '24px 0',
}
const link = {
color: '#5046e5',
textDecoration: 'underline',
wordBreak: 'break-all' as const,
}
const hr = {
borderColor: '#e6ebf1',
margin: '32px 0',
}
const footer = {
padding: '0 48px',
}
const footerText = {
fontSize: '12px',
color: '#8898aa',
}
const footerLink = {
fontSize: '12px',
color: '#8898aa',
}Wysyłanie z React Email
import { resend } from '@/lib/resend'
import WelcomeEmail from '@/emails/welcome'
export async function sendWelcomeEmail(
email: string,
username: string,
verificationToken: string
) {
const verificationUrl = `https://yourdomain.com/verify?token=${verificationToken}`
const { data, error } = await resend.emails.send({
from: 'CodeWorlds <welcome@yourdomain.com>',
to: email,
subject: `Witaj w CodeWorlds, ${username}!`,
react: WelcomeEmail({ username, verificationUrl }),
})
if (error) {
throw new Error(`Failed to send welcome email: ${error.message}`)
}
return data
}Podgląd szablonów lokalnie
// package.json
{
"scripts": {
"email:dev": "email dev --dir emails --port 3001"
}
}npm run email:dev
# Otwórz http://localhost:3001 dla podglądu szablonówBiblioteka szablonów emaili
Email potwierdzenia zamówienia
// emails/order-confirmation.tsx
import {
Html,
Head,
Body,
Container,
Section,
Row,
Column,
Text,
Button,
Img,
Hr,
} from '@react-email/components'
interface OrderItem {
name: string
quantity: number
price: number
imageUrl: string
}
interface OrderConfirmationProps {
orderNumber: string
customerName: string
items: OrderItem[]
subtotal: number
shipping: number
total: number
shippingAddress: string
trackingUrl: string
}
export default function OrderConfirmation({
orderNumber,
customerName,
items,
subtotal,
shipping,
total,
shippingAddress,
trackingUrl,
}: OrderConfirmationProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Section style={header}>
<Text style={heading}>Potwierdzenie zamówienia</Text>
<Text style={orderNum}>Zamówienie #{orderNumber}</Text>
</Section>
<Section style={section}>
<Text style={greeting}>Cześć {customerName}!</Text>
<Text style={text}>
Dziękujemy za zamówienie. Oto podsumowanie:
</Text>
</Section>
<Section style={itemsSection}>
{items.map((item, index) => (
<Row key={index} style={itemRow}>
<Column style={imageColumn}>
<Img
src={item.imageUrl}
width={64}
height={64}
alt={item.name}
style={itemImage}
/>
</Column>
<Column style={detailsColumn}>
<Text style={itemName}>{item.name}</Text>
<Text style={itemQuantity}>Ilość: {item.quantity}</Text>
</Column>
<Column style={priceColumn}>
<Text style={itemPrice}>{item.price.toFixed(2)} zł</Text>
</Column>
</Row>
))}
</Section>
<Hr style={hr} />
<Section style={summarySection}>
<Row>
<Column><Text style={summaryLabel}>Produkty:</Text></Column>
<Column><Text style={summaryValue}>{subtotal.toFixed(2)} zł</Text></Column>
</Row>
<Row>
<Column><Text style={summaryLabel}>Dostawa:</Text></Column>
<Column><Text style={summaryValue}>{shipping.toFixed(2)} zł</Text></Column>
</Row>
<Row>
<Column><Text style={totalLabel}>Razem:</Text></Column>
<Column><Text style={totalValue}>{total.toFixed(2)} zł</Text></Column>
</Row>
</Section>
<Section style={section}>
<Text style={addressLabel}>Adres dostawy:</Text>
<Text style={address}>{shippingAddress}</Text>
</Section>
<Section style={ctaSection}>
<Button style={button} href={trackingUrl}>
Śledź przesyłkę
</Button>
</Section>
</Container>
</Body>
</Html>
)
}
const main = { backgroundColor: '#f6f9fc', fontFamily: 'Arial, sans-serif' }
const container = { backgroundColor: '#ffffff', margin: '0 auto', padding: '40px' }
const header = { textAlign: 'center' as const, marginBottom: '32px' }
const heading = { fontSize: '28px', fontWeight: 'bold', color: '#1a1a1a' }
const orderNum = { fontSize: '14px', color: '#666' }
const greeting = { fontSize: '18px', fontWeight: '600' }
const text = { fontSize: '16px', color: '#333', lineHeight: '24px' }
const section = { marginBottom: '24px' }
const itemsSection = { backgroundColor: '#f9fafb', padding: '16px', borderRadius: '8px' }
const itemRow = { marginBottom: '16px' }
const imageColumn = { width: '80px' }
const detailsColumn = { paddingLeft: '16px' }
const priceColumn = { textAlign: 'right' as const }
const itemImage = { borderRadius: '8px' }
const itemName = { fontSize: '14px', fontWeight: '600', margin: '0' }
const itemQuantity = { fontSize: '12px', color: '#666', margin: '4px 0 0' }
const itemPrice = { fontSize: '14px', fontWeight: '600' }
const hr = { borderColor: '#e6ebf1', margin: '24px 0' }
const summarySection = { marginBottom: '24px' }
const summaryLabel = { fontSize: '14px', color: '#666' }
const summaryValue = { fontSize: '14px', textAlign: 'right' as const }
const totalLabel = { fontSize: '16px', fontWeight: 'bold' }
const totalValue = { fontSize: '16px', fontWeight: 'bold', textAlign: 'right' as const }
const addressLabel = { fontSize: '14px', fontWeight: '600', marginBottom: '8px' }
const address = { fontSize: '14px', color: '#666', whiteSpace: 'pre-line' as const }
const ctaSection = { textAlign: 'center' as const }
const button = {
backgroundColor: '#000',
color: '#fff',
padding: '12px 32px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: 'bold',
textDecoration: 'none',
}Email resetowania hasła
// emails/password-reset.tsx
import {
Html,
Head,
Body,
Container,
Section,
Text,
Button,
Link,
} from '@react-email/components'
interface PasswordResetProps {
resetUrl: string
expiresIn: string
ipAddress: string
userAgent: string
}
export default function PasswordReset({
resetUrl,
expiresIn,
ipAddress,
userAgent,
}: PasswordResetProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Section style={section}>
<Text style={heading}>Reset hasła</Text>
<Text style={text}>
Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta.
Kliknij poniższy przycisk, aby ustawić nowe hasło.
</Text>
<Button style={button} href={resetUrl}>
Zresetuj hasło
</Button>
<Text style={text}>
Link jest ważny przez {expiresIn}. Jeśli nie prosiłeś o reset
hasła, zignoruj tę wiadomość.
</Text>
<Section style={securitySection}>
<Text style={securityHeading}>Szczegóły żądania:</Text>
<Text style={securityText}>IP: {ipAddress}</Text>
<Text style={securityText}>Przeglądarka: {userAgent}</Text>
</Section>
<Text style={footerText}>
Jeśli nie rozpoznajesz tej aktywności,{' '}
<Link href="https://yourdomain.com/security" style={link}>
zabezpiecz swoje konto
</Link>
.
</Text>
</Section>
</Container>
</Body>
</Html>
)
}
const main = { backgroundColor: '#f6f9fc', fontFamily: 'Arial, sans-serif' }
const container = { backgroundColor: '#ffffff', margin: '0 auto', padding: '40px' }
const section = { padding: '0' }
const heading = { fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }
const text = { fontSize: '16px', lineHeight: '26px', color: '#333' }
const button = {
backgroundColor: '#dc2626',
color: '#fff',
padding: '14px 32px',
borderRadius: '6px',
fontSize: '16px',
fontWeight: 'bold',
textDecoration: 'none',
display: 'block',
textAlign: 'center' as const,
margin: '24px 0',
}
const securitySection = {
backgroundColor: '#fef2f2',
padding: '16px',
borderRadius: '8px',
marginTop: '24px',
}
const securityHeading = { fontSize: '14px', fontWeight: '600', margin: '0 0 8px' }
const securityText = { fontSize: '12px', color: '#666', margin: '4px 0' }
const footerText = { fontSize: '14px', color: '#666', marginTop: '24px' }
const link = { color: '#dc2626' }Integracja z Next.js
API Route (App Router)
// app/api/send-email/route.ts
import { NextResponse } from 'next/server'
import { resend } from '@/lib/resend'
import WelcomeEmail from '@/emails/welcome'
export async function POST(request: Request) {
try {
const { email, username, verificationToken } = await request.json()
if (!email || !username) {
return NextResponse.json(
{ error: 'Email and username are required' },
{ status: 400 }
)
}
const verificationUrl = `${process.env.NEXT_PUBLIC_APP_URL}/verify?token=${verificationToken}`
const { data, error } = await resend.emails.send({
from: 'CodeWorlds <noreply@yourdomain.com>',
to: email,
subject: `Witaj w CodeWorlds, ${username}!`,
react: WelcomeEmail({ username, verificationUrl }),
})
if (error) {
console.error('Resend error:', error)
return NextResponse.json(
{ error: 'Failed to send email' },
{ status: 500 }
)
}
return NextResponse.json({ id: data.id })
} catch (error) {
console.error('Server error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}Server Action
// app/actions/email.ts
'use server'
import { resend } from '@/lib/resend'
import ContactEmail from '@/emails/contact'
interface ContactFormData {
name: string
email: string
subject: string
message: string
}
export async function sendContactEmail(formData: ContactFormData) {
try {
const { data, error } = await resend.emails.send({
from: 'Contact Form <contact@yourdomain.com>',
to: 'team@yourdomain.com',
reply_to: formData.email,
subject: `[Kontakt] ${formData.subject}`,
react: ContactEmail({
name: formData.name,
email: formData.email,
message: formData.message,
}),
})
if (error) {
return { success: false, error: error.message }
}
return { success: true, id: data.id }
} catch (error) {
return { success: false, error: 'Failed to send email' }
}
}Formularz kontaktowy
// components/ContactForm.tsx
'use client'
import { useState } from 'react'
import { sendContactEmail } from '@/app/actions/email'
export function ContactForm() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setIsSubmitting(true)
setMessage(null)
const formData = new FormData(e.currentTarget)
const result = await sendContactEmail({
name: formData.get('name') as string,
email: formData.get('email') as string,
subject: formData.get('subject') as string,
message: formData.get('message') as string,
})
setIsSubmitting(false)
if (result.success) {
setMessage({ type: 'success', text: 'Wiadomość wysłana!' })
e.currentTarget.reset()
} else {
setMessage({ type: 'error', text: result.error || 'Wystąpił błąd' })
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium">
Imię i nazwisko
</label>
<input
type="text"
id="name"
name="name"
required
className="mt-1 block w-full rounded-md border px-3 py-2"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
type="email"
id="email"
name="email"
required
className="mt-1 block w-full rounded-md border px-3 py-2"
/>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium">
Temat
</label>
<input
type="text"
id="subject"
name="subject"
required
className="mt-1 block w-full rounded-md border px-3 py-2"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium">
Wiadomość
</label>
<textarea
id="message"
name="message"
rows={4}
required
className="mt-1 block w-full rounded-md border px-3 py-2"
/>
</div>
{message && (
<div className={`p-3 rounded ${
message.type === 'success' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{message.text}
</div>
)}
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting ? 'Wysyłanie...' : 'Wyślij wiadomość'}
</button>
</form>
)
}Webhooks - Śledzenie statusu emaili
Konfiguracja webhooków
Resend może wysyłać webhooks przy różnych wydarzeniach:
email.sent- Email został wysłanyemail.delivered- Email został dostarczonyemail.opened- Email został otwartyemail.clicked- Link w emailu został klikniętyemail.bounced- Email odbił sięemail.complained- Użytkownik oznaczył email jako spam
Endpoint webhooks
// app/api/webhooks/resend/route.ts
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'
import crypto from 'crypto'
const RESEND_WEBHOOK_SECRET = process.env.RESEND_WEBHOOK_SECRET!
interface ResendWebhookPayload {
type: string
created_at: string
data: {
email_id: string
from: string
to: string[]
subject: string
created_at: string
tags?: { name: string; value: string }[]
}
}
function verifySignature(payload: string, signature: string): boolean {
const expectedSignature = crypto
.createHmac('sha256', RESEND_WEBHOOK_SECRET)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)
}
export async function POST(request: Request) {
try {
const headersList = headers()
const signature = headersList.get('resend-signature')
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
)
}
const body = await request.text()
if (!verifySignature(body, signature)) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
)
}
const payload: ResendWebhookPayload = JSON.parse(body)
// Przetwarzanie różnych typów wydarzeń
switch (payload.type) {
case 'email.sent':
console.log('Email sent:', payload.data.email_id)
await handleEmailSent(payload.data)
break
case 'email.delivered':
console.log('Email delivered:', payload.data.email_id)
await handleEmailDelivered(payload.data)
break
case 'email.opened':
console.log('Email opened:', payload.data.email_id)
await handleEmailOpened(payload.data)
break
case 'email.clicked':
console.log('Email clicked:', payload.data.email_id)
await handleEmailClicked(payload.data)
break
case 'email.bounced':
console.log('Email bounced:', payload.data.email_id)
await handleEmailBounced(payload.data)
break
case 'email.complained':
console.log('Email marked as spam:', payload.data.email_id)
await handleEmailComplained(payload.data)
break
default:
console.log('Unknown event type:', payload.type)
}
return NextResponse.json({ received: true })
} catch (error) {
console.error('Webhook error:', error)
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
)
}
}
// Handlers
async function handleEmailSent(data: ResendWebhookPayload['data']) {
// Zapisz status w bazie danych
}
async function handleEmailDelivered(data: ResendWebhookPayload['data']) {
// Aktualizuj status w bazie
}
async function handleEmailOpened(data: ResendWebhookPayload['data']) {
// Zapisz otwarcie dla analityki
}
async function handleEmailClicked(data: ResendWebhookPayload['data']) {
// Śledź kliknięcia linków
}
async function handleEmailBounced(data: ResendWebhookPayload['data']) {
// Oznacz email jako nieaktywny
// Usuń z listy mailingowej
}
async function handleEmailComplained(data: ResendWebhookPayload['data']) {
// Natychmiast usuń z wszystkich list
// Zapisz w blacklist
}Zarządzanie domenami
Weryfikacja domeny
import { resend } from '@/lib/resend'
// Dodanie domeny
const { data: domain, error } = await resend.domains.create({
name: 'yourdomain.com',
region: 'eu-west-1', // lub 'us-east-1'
})
// Lista domen
const { data: domains } = await resend.domains.list()
// Weryfikacja domeny
const { data: verified } = await resend.domains.verify('domain_id')
// Szczegóły domeny (rekordy DNS)
const { data: domainDetails } = await resend.domains.get('domain_id')
console.log(domainDetails.records) // Rekordy DNS do dodaniaRekordy DNS
Po dodaniu domeny, Resend zwróci rekordy DNS do skonfigurowania:
// Przykładowa odpowiedź
{
records: [
{
type: 'MX',
name: 'send',
value: 'feedback-smtp.eu-west-1.amazonses.com',
priority: 10
},
{
type: 'TXT',
name: 'send',
value: 'v=spf1 include:amazonses.com ~all'
},
{
type: 'TXT',
name: 'resend._domainkey',
value: 'p=MIGfMA0GCSqGSIb3DQEBAQUAA...'
}
]
}Audience i kontakty
Zarządzanie listami odbiorców
import { resend } from '@/lib/resend'
// Tworzenie audience (listy)
const { data: audience } = await resend.audiences.create({
name: 'Newsletter Subscribers'
})
// Dodawanie kontaktu
const { data: contact } = await resend.contacts.create({
audience_id: audience.id,
email: 'user@example.com',
first_name: 'Jan',
last_name: 'Kowalski',
unsubscribed: false,
})
// Pobieranie kontaktów
const { data: contacts } = await resend.contacts.list({
audience_id: audience.id,
})
// Aktualizacja kontaktu
await resend.contacts.update({
audience_id: audience.id,
id: contact.id,
first_name: 'John',
})
// Usunięcie kontaktu
await resend.contacts.remove({
audience_id: audience.id,
id: contact.id,
})Wysyłka do audience
const { data, error } = await resend.emails.send({
from: 'Newsletter <newsletter@yourdomain.com>',
to: audience.id, // ID audience zamiast konkretnego emaila
subject: 'Nowy artykuł na blogu',
react: NewsletterEmail({ title: 'Artykuł', content: '...' }),
})Batch sending
Wysyłanie wielu emaili naraz
import { resend } from '@/lib/resend'
const emails = [
{
from: 'newsletter@yourdomain.com',
to: 'user1@example.com',
subject: 'Newsletter #1',
html: '<p>Content 1</p>',
},
{
from: 'newsletter@yourdomain.com',
to: 'user2@example.com',
subject: 'Newsletter #1',
html: '<p>Content 2</p>',
},
// ... więcej emaili
]
const { data, error } = await resend.batch.send(emails)
// data zawiera array z ID każdego emaila
console.log(data) // [{ id: 'email_1' }, { id: 'email_2' }]Personalizowane batch sending
interface Subscriber {
email: string
name: string
preferences: string[]
}
async function sendPersonalizedNewsletter(subscribers: Subscriber[]) {
const emails = subscribers.map(subscriber => ({
from: 'newsletter@yourdomain.com',
to: subscriber.email,
subject: `Cześć ${subscriber.name}! Nowy newsletter`,
react: NewsletterEmail({
name: subscriber.name,
topics: subscriber.preferences,
}),
tags: [
{ name: 'campaign', value: 'weekly-newsletter' },
{ name: 'subscriber_id', value: subscriber.email },
],
}))
// Batch po 100 emaili (limit Resend)
const batches = []
for (let i = 0; i < emails.length; i += 100) {
batches.push(emails.slice(i, i + 100))
}
const results = []
for (const batch of batches) {
const { data, error } = await resend.batch.send(batch)
if (error) {
console.error('Batch error:', error)
}
results.push(...(data || []))
// Czekaj między batchami
await new Promise(resolve => setTimeout(resolve, 1000))
}
return results
}Rate limiting i obsługa błędów
Implementacja rate limiting
import { resend } from '@/lib/resend'
class EmailService {
private queue: Array<() => Promise<void>> = []
private processing = false
private rateLimit = 10 // emails per second
async send(params: Parameters<typeof resend.emails.send>[0]) {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const result = await this.sendWithRetry(params)
resolve(result)
} catch (error) {
reject(error)
}
})
this.processQueue()
})
}
private async processQueue() {
if (this.processing) return
this.processing = true
while (this.queue.length > 0) {
const batch = this.queue.splice(0, this.rateLimit)
await Promise.all(batch.map(fn => fn()))
await new Promise(resolve => setTimeout(resolve, 1000))
}
this.processing = false
}
private async sendWithRetry(
params: Parameters<typeof resend.emails.send>[0],
retries = 3
) {
for (let i = 0; i < retries; i++) {
const { data, error } = await resend.emails.send(params)
if (!error) {
return data
}
// Retry na rate limit lub server error
if (error.statusCode === 429 || error.statusCode >= 500) {
const delay = Math.pow(2, i) * 1000 // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay))
continue
}
// Nie retry na inne błędy
throw error
}
throw new Error('Max retries exceeded')
}
}
export const emailService = new EmailService()Best practices
1. Używaj tagów do śledzenia
await resend.emails.send({
// ...
tags: [
{ name: 'type', value: 'transactional' },
{ name: 'campaign', value: 'welcome-series' },
{ name: 'user_id', value: userId },
],
})2. Zawsze waliduj adresy email
import { z } from 'zod'
const emailSchema = z.string().email()
async function sendEmail(to: string, subject: string, content: string) {
const validatedEmail = emailSchema.parse(to)
await resend.emails.send({
from: 'noreply@yourdomain.com',
to: validatedEmail,
subject,
html: content,
})
}3. Obsługuj unsubscribe
// W każdym marketingowym emailu
<Text style={footerText}>
Nie chcesz otrzymywać tych wiadomości?{' '}
<Link href={`https://yourdomain.com/unsubscribe?email=${encodeURIComponent(email)}`}>
Wypisz się
</Link>
</Text>4. Testuj szablony przed wysyłką
// Użyj onboarding@resend.dev do testów
const { data, error } = await resend.emails.send({
from: 'onboarding@resend.dev', // Testowa domena Resend
to: 'test@example.com',
subject: 'Test email',
react: TestEmail(),
})Cennik i limity
| Plan | Emaile/miesiąc | Cena | Limity |
|---|---|---|---|
| Free | 3,000 | $0 | 100/dzień |
| Pro | 50,000 | $20/mo | + $0.28/1000 over |
| Scale | 100,000 | $90/mo | + $0.25/1000 over |
| Enterprise | Custom | Custom | Dedykowana infrastruktura |
Rate limits
- Free: 2 emails/second
- Pro: 10 emails/second
- Scale: 50 emails/second
- Batch API: max 100 emails per request
FAQ - Często zadawane pytania
Czy Resend jest lepszy od SendGrid?
Resend oferuje lepsze developer experience dzięki React Email i nowoczesnemu API. SendGrid ma więcej funkcji marketingowych, ale Resend jest prostszy do integracji w aplikacjach Next.js.
Jak wysyłać emaile z załącznikami?
Użyj pola attachments z Buffer lub base64:
const { data } = await resend.emails.send({
// ...
attachments: [
{
filename: 'report.pdf',
content: Buffer.from(pdfData),
},
],
})Czy mogę używać własnej domeny od razu?
Tak, ale wymaga weryfikacji DNS. Do testów możesz używać onboarding@resend.dev.
Jak śledzić otwarcia emaili?
Włącz tracking w dashboardzie Resend i obsługuj webhook email.opened.
Podsumowanie
Resend to nowoczesne rozwiązanie do wysyłki emaili, które idealnie pasuje do ekosystemu React i Next.js. Główne zalety:
- React Email - tworzenie szablonów jako komponenty React
- Type-safe API - pełne wsparcie TypeScript
- Wysoka dostarczalność - dedykowana infrastruktura
- Prostota - intuicyjne API bez zbędnej konfiguracji
- Webhooks - śledzenie statusu w czasie rzeczywistym
Jeśli budujesz aplikację w Next.js i potrzebujesz niezawodnego systemu wysyłki emaili transakcyjnych, Resend jest doskonałym wyborem.
Resend - Modern email API for developers
What is Resend?
Resend is a modern email sending platform, created by the makers of React Email. Unlike traditional SMTP solutions, Resend offers a clean, developer-friendly API with native TypeScript and React support. It is the ideal tool for Next.js applications that need a reliable transactional email delivery system.
Why Resend?
Problems with traditional solutions
Sending emails has always been problematic for developers:
- SendGrid/Mailgun: Complex dashboards, poor DX
- Nodemailer: Requires your own SMTP server, deliverability issues
- AWS SES: Difficult configuration, strict limits
- Direct SMTP: IP reputation problems, blacklists
Resend advantages
- React Email - Build email templates as React components
- Type-safe API - Full TypeScript support
- High deliverability - Dedicated infrastructure with monitoring
- Simple API - Intuitive, no unnecessary configuration
- Webhooks - Real-time email status tracking
- Domain verification - Easy custom domain setup
Installation and configuration
Package installation
# Main Resend package
npm install resend
# React Email for templates (optional, but recommended)
npm install @react-email/components
# For previewing templates locally
npm install react-email --save-devAPI key configuration
Create an account at resend.com and generate an API key:
// lib/resend.ts
import { Resend } from 'resend'
if (!process.env.RESEND_API_KEY) {
throw new Error('Missing RESEND_API_KEY environment variable')
}
export const resend = new Resend(process.env.RESEND_API_KEY)# .env.local
RESEND_API_KEY=re_xxxxxxxxxxxxxBasic email sending
Simple text email
import { resend } from '@/lib/resend'
const { data, error } = await resend.emails.send({
from: 'hello@yourdomain.com',
to: 'user@example.com',
subject: 'Welcome to our application!',
text: 'Thank you for signing up. Your account is now active.',
})
if (error) {
console.error('Failed to send email:', error)
return
}
console.log('Email sent:', data.id)HTML email
const { data, error } = await resend.emails.send({
from: 'notifications@yourdomain.com',
to: ['user1@example.com', 'user2@example.com'],
subject: 'New order #12345',
html: `
<h1>Order confirmation</h1>
<p>Thank you for placing your order!</p>
<p>Order number: <strong>#12345</strong></p>
<a href="https://example.com/orders/12345">View details</a>
`,
})Advanced options
const { data, error } = await resend.emails.send({
from: 'John Smith <john@yourdomain.com>',
to: 'user@example.com',
cc: ['manager@company.com'],
bcc: ['archive@company.com'],
reply_to: 'support@yourdomain.com',
subject: 'Important message',
html: '<p>Message content</p>',
// Attachments
attachments: [
{
filename: 'report.pdf',
content: pdfBuffer, // Buffer or base64 string
},
],
// Tags for tracking
tags: [
{ name: 'category', value: 'order_confirmation' },
{ name: 'user_id', value: '12345' },
],
// Scheduled sending
scheduled_at: '2024-12-25T09:00:00Z',
// Headers
headers: {
'X-Entity-Ref-ID': 'order-12345',
},
})React Email - Templates as components
Creating a template
React Email allows you to build email templates as React components:
// emails/welcome.tsx
import {
Html,
Head,
Body,
Container,
Section,
Text,
Button,
Img,
Link,
Preview,
Hr,
} from '@react-email/components'
interface WelcomeEmailProps {
username: string
verificationUrl: string
}
export default function WelcomeEmail({
username,
verificationUrl,
}: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to CodeWorlds, {username}!</Preview>
<Body style={main}>
<Container style={container}>
<Img
src="https://yourdomain.com/logo.png"
width={150}
height={50}
alt="CodeWorlds"
/>
<Section style={section}>
<Text style={heading}>Welcome, {username}!</Text>
<Text style={text}>
Thank you for joining CodeWorlds. Your programming adventure
is just beginning!
</Text>
<Button style={button} href={verificationUrl}>
Activate account
</Button>
<Text style={text}>
Or copy this link into your browser:
</Text>
<Link href={verificationUrl} style={link}>
{verificationUrl}
</Link>
</Section>
<Hr style={hr} />
<Section style={footer}>
<Text style={footerText}>
© 2024 CodeWorlds. All rights reserved.
</Text>
<Link href="https://yourdomain.com/unsubscribe" style={footerLink}>
Unsubscribe from newsletter
</Link>
</Section>
</Container>
</Body>
</Html>
)
}
// Inline styles (required for emails)
const main = {
backgroundColor: '#f6f9fc',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}
const container = {
backgroundColor: '#ffffff',
margin: '0 auto',
padding: '20px 0 48px',
marginBottom: '64px',
}
const section = {
padding: '0 48px',
}
const heading = {
fontSize: '24px',
fontWeight: 'bold',
marginBottom: '16px',
}
const text = {
fontSize: '16px',
lineHeight: '26px',
color: '#333',
}
const button = {
backgroundColor: '#5046e5',
borderRadius: '6px',
color: '#fff',
fontSize: '16px',
fontWeight: 'bold',
textDecoration: 'none',
textAlign: 'center' as const,
display: 'block',
padding: '12px 24px',
margin: '24px 0',
}
const link = {
color: '#5046e5',
textDecoration: 'underline',
wordBreak: 'break-all' as const,
}
const hr = {
borderColor: '#e6ebf1',
margin: '32px 0',
}
const footer = {
padding: '0 48px',
}
const footerText = {
fontSize: '12px',
color: '#8898aa',
}
const footerLink = {
fontSize: '12px',
color: '#8898aa',
}Sending with React Email
import { resend } from '@/lib/resend'
import WelcomeEmail from '@/emails/welcome'
export async function sendWelcomeEmail(
email: string,
username: string,
verificationToken: string
) {
const verificationUrl = `https://yourdomain.com/verify?token=${verificationToken}`
const { data, error } = await resend.emails.send({
from: 'CodeWorlds <welcome@yourdomain.com>',
to: email,
subject: `Welcome to CodeWorlds, ${username}!`,
react: WelcomeEmail({ username, verificationUrl }),
})
if (error) {
throw new Error(`Failed to send welcome email: ${error.message}`)
}
return data
}Previewing templates locally
// package.json
{
"scripts": {
"email:dev": "email dev --dir emails --port 3001"
}
}npm run email:dev
# Open http://localhost:3001 to preview templatesEmail template library
Order confirmation email
// emails/order-confirmation.tsx
import {
Html,
Head,
Body,
Container,
Section,
Row,
Column,
Text,
Button,
Img,
Hr,
} from '@react-email/components'
interface OrderItem {
name: string
quantity: number
price: number
imageUrl: string
}
interface OrderConfirmationProps {
orderNumber: string
customerName: string
items: OrderItem[]
subtotal: number
shipping: number
total: number
shippingAddress: string
trackingUrl: string
}
export default function OrderConfirmation({
orderNumber,
customerName,
items,
subtotal,
shipping,
total,
shippingAddress,
trackingUrl,
}: OrderConfirmationProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Section style={header}>
<Text style={heading}>Order confirmation</Text>
<Text style={orderNum}>Order #{orderNumber}</Text>
</Section>
<Section style={section}>
<Text style={greeting}>Hi {customerName}!</Text>
<Text style={text}>
Thank you for your order. Here is the summary:
</Text>
</Section>
<Section style={itemsSection}>
{items.map((item, index) => (
<Row key={index} style={itemRow}>
<Column style={imageColumn}>
<Img
src={item.imageUrl}
width={64}
height={64}
alt={item.name}
style={itemImage}
/>
</Column>
<Column style={detailsColumn}>
<Text style={itemName}>{item.name}</Text>
<Text style={itemQuantity}>Qty: {item.quantity}</Text>
</Column>
<Column style={priceColumn}>
<Text style={itemPrice}>${item.price.toFixed(2)}</Text>
</Column>
</Row>
))}
</Section>
<Hr style={hr} />
<Section style={summarySection}>
<Row>
<Column><Text style={summaryLabel}>Products:</Text></Column>
<Column><Text style={summaryValue}>${subtotal.toFixed(2)}</Text></Column>
</Row>
<Row>
<Column><Text style={summaryLabel}>Shipping:</Text></Column>
<Column><Text style={summaryValue}>${shipping.toFixed(2)}</Text></Column>
</Row>
<Row>
<Column><Text style={totalLabel}>Total:</Text></Column>
<Column><Text style={totalValue}>${total.toFixed(2)}</Text></Column>
</Row>
</Section>
<Section style={section}>
<Text style={addressLabel}>Shipping address:</Text>
<Text style={address}>{shippingAddress}</Text>
</Section>
<Section style={ctaSection}>
<Button style={button} href={trackingUrl}>
Track shipment
</Button>
</Section>
</Container>
</Body>
</Html>
)
}
const main = { backgroundColor: '#f6f9fc', fontFamily: 'Arial, sans-serif' }
const container = { backgroundColor: '#ffffff', margin: '0 auto', padding: '40px' }
const header = { textAlign: 'center' as const, marginBottom: '32px' }
const heading = { fontSize: '28px', fontWeight: 'bold', color: '#1a1a1a' }
const orderNum = { fontSize: '14px', color: '#666' }
const greeting = { fontSize: '18px', fontWeight: '600' }
const text = { fontSize: '16px', color: '#333', lineHeight: '24px' }
const section = { marginBottom: '24px' }
const itemsSection = { backgroundColor: '#f9fafb', padding: '16px', borderRadius: '8px' }
const itemRow = { marginBottom: '16px' }
const imageColumn = { width: '80px' }
const detailsColumn = { paddingLeft: '16px' }
const priceColumn = { textAlign: 'right' as const }
const itemImage = { borderRadius: '8px' }
const itemName = { fontSize: '14px', fontWeight: '600', margin: '0' }
const itemQuantity = { fontSize: '12px', color: '#666', margin: '4px 0 0' }
const itemPrice = { fontSize: '14px', fontWeight: '600' }
const hr = { borderColor: '#e6ebf1', margin: '24px 0' }
const summarySection = { marginBottom: '24px' }
const summaryLabel = { fontSize: '14px', color: '#666' }
const summaryValue = { fontSize: '14px', textAlign: 'right' as const }
const totalLabel = { fontSize: '16px', fontWeight: 'bold' }
const totalValue = { fontSize: '16px', fontWeight: 'bold', textAlign: 'right' as const }
const addressLabel = { fontSize: '14px', fontWeight: '600', marginBottom: '8px' }
const address = { fontSize: '14px', color: '#666', whiteSpace: 'pre-line' as const }
const ctaSection = { textAlign: 'center' as const }
const button = {
backgroundColor: '#000',
color: '#fff',
padding: '12px 32px',
borderRadius: '6px',
fontSize: '14px',
fontWeight: 'bold',
textDecoration: 'none',
}Password reset email
// emails/password-reset.tsx
import {
Html,
Head,
Body,
Container,
Section,
Text,
Button,
Link,
} from '@react-email/components'
interface PasswordResetProps {
resetUrl: string
expiresIn: string
ipAddress: string
userAgent: string
}
export default function PasswordReset({
resetUrl,
expiresIn,
ipAddress,
userAgent,
}: PasswordResetProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Section style={section}>
<Text style={heading}>Password reset</Text>
<Text style={text}>
We received a request to reset the password for your account.
Click the button below to set a new password.
</Text>
<Button style={button} href={resetUrl}>
Reset password
</Button>
<Text style={text}>
The link is valid for {expiresIn}. If you did not request a password
reset, please ignore this message.
</Text>
<Section style={securitySection}>
<Text style={securityHeading}>Request details:</Text>
<Text style={securityText}>IP: {ipAddress}</Text>
<Text style={securityText}>Browser: {userAgent}</Text>
</Section>
<Text style={footerText}>
If you do not recognize this activity,{' '}
<Link href="https://yourdomain.com/security" style={link}>
secure your account
</Link>
.
</Text>
</Section>
</Container>
</Body>
</Html>
)
}
const main = { backgroundColor: '#f6f9fc', fontFamily: 'Arial, sans-serif' }
const container = { backgroundColor: '#ffffff', margin: '0 auto', padding: '40px' }
const section = { padding: '0' }
const heading = { fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }
const text = { fontSize: '16px', lineHeight: '26px', color: '#333' }
const button = {
backgroundColor: '#dc2626',
color: '#fff',
padding: '14px 32px',
borderRadius: '6px',
fontSize: '16px',
fontWeight: 'bold',
textDecoration: 'none',
display: 'block',
textAlign: 'center' as const,
margin: '24px 0',
}
const securitySection = {
backgroundColor: '#fef2f2',
padding: '16px',
borderRadius: '8px',
marginTop: '24px',
}
const securityHeading = { fontSize: '14px', fontWeight: '600', margin: '0 0 8px' }
const securityText = { fontSize: '12px', color: '#666', margin: '4px 0' }
const footerText = { fontSize: '14px', color: '#666', marginTop: '24px' }
const link = { color: '#dc2626' }Next.js integration
API Route (App Router)
// app/api/send-email/route.ts
import { NextResponse } from 'next/server'
import { resend } from '@/lib/resend'
import WelcomeEmail from '@/emails/welcome'
export async function POST(request: Request) {
try {
const { email, username, verificationToken } = await request.json()
if (!email || !username) {
return NextResponse.json(
{ error: 'Email and username are required' },
{ status: 400 }
)
}
const verificationUrl = `${process.env.NEXT_PUBLIC_APP_URL}/verify?token=${verificationToken}`
const { data, error } = await resend.emails.send({
from: 'CodeWorlds <noreply@yourdomain.com>',
to: email,
subject: `Welcome to CodeWorlds, ${username}!`,
react: WelcomeEmail({ username, verificationUrl }),
})
if (error) {
console.error('Resend error:', error)
return NextResponse.json(
{ error: 'Failed to send email' },
{ status: 500 }
)
}
return NextResponse.json({ id: data.id })
} catch (error) {
console.error('Server error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}Server Action
// app/actions/email.ts
'use server'
import { resend } from '@/lib/resend'
import ContactEmail from '@/emails/contact'
interface ContactFormData {
name: string
email: string
subject: string
message: string
}
export async function sendContactEmail(formData: ContactFormData) {
try {
const { data, error } = await resend.emails.send({
from: 'Contact Form <contact@yourdomain.com>',
to: 'team@yourdomain.com',
reply_to: formData.email,
subject: `[Contact] ${formData.subject}`,
react: ContactEmail({
name: formData.name,
email: formData.email,
message: formData.message,
}),
})
if (error) {
return { success: false, error: error.message }
}
return { success: true, id: data.id }
} catch (error) {
return { success: false, error: 'Failed to send email' }
}
}Contact form
// components/ContactForm.tsx
'use client'
import { useState } from 'react'
import { sendContactEmail } from '@/app/actions/email'
export function ContactForm() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setIsSubmitting(true)
setMessage(null)
const formData = new FormData(e.currentTarget)
const result = await sendContactEmail({
name: formData.get('name') as string,
email: formData.get('email') as string,
subject: formData.get('subject') as string,
message: formData.get('message') as string,
})
setIsSubmitting(false)
if (result.success) {
setMessage({ type: 'success', text: 'Message sent!' })
e.currentTarget.reset()
} else {
setMessage({ type: 'error', text: result.error || 'An error occurred' })
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium">
Full name
</label>
<input
type="text"
id="name"
name="name"
required
className="mt-1 block w-full rounded-md border px-3 py-2"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
type="email"
id="email"
name="email"
required
className="mt-1 block w-full rounded-md border px-3 py-2"
/>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium">
Subject
</label>
<input
type="text"
id="subject"
name="subject"
required
className="mt-1 block w-full rounded-md border px-3 py-2"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium">
Message
</label>
<textarea
id="message"
name="message"
rows={4}
required
className="mt-1 block w-full rounded-md border px-3 py-2"
/>
</div>
{message && (
<div className={`p-3 rounded ${
message.type === 'success' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{message.text}
</div>
)}
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting ? 'Sending...' : 'Send message'}
</button>
</form>
)
}Webhooks - Email status tracking
Webhook configuration
Resend can send webhooks for various events:
email.sent- Email has been sentemail.delivered- Email has been deliveredemail.opened- Email has been openedemail.clicked- A link in the email has been clickedemail.bounced- Email has bouncedemail.complained- User marked the email as spam
Webhook endpoint
// app/api/webhooks/resend/route.ts
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'
import crypto from 'crypto'
const RESEND_WEBHOOK_SECRET = process.env.RESEND_WEBHOOK_SECRET!
interface ResendWebhookPayload {
type: string
created_at: string
data: {
email_id: string
from: string
to: string[]
subject: string
created_at: string
tags?: { name: string; value: string }[]
}
}
function verifySignature(payload: string, signature: string): boolean {
const expectedSignature = crypto
.createHmac('sha256', RESEND_WEBHOOK_SECRET)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)
}
export async function POST(request: Request) {
try {
const headersList = headers()
const signature = headersList.get('resend-signature')
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 401 }
)
}
const body = await request.text()
if (!verifySignature(body, signature)) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
)
}
const payload: ResendWebhookPayload = JSON.parse(body)
// Processing different event types
switch (payload.type) {
case 'email.sent':
console.log('Email sent:', payload.data.email_id)
await handleEmailSent(payload.data)
break
case 'email.delivered':
console.log('Email delivered:', payload.data.email_id)
await handleEmailDelivered(payload.data)
break
case 'email.opened':
console.log('Email opened:', payload.data.email_id)
await handleEmailOpened(payload.data)
break
case 'email.clicked':
console.log('Email clicked:', payload.data.email_id)
await handleEmailClicked(payload.data)
break
case 'email.bounced':
console.log('Email bounced:', payload.data.email_id)
await handleEmailBounced(payload.data)
break
case 'email.complained':
console.log('Email marked as spam:', payload.data.email_id)
await handleEmailComplained(payload.data)
break
default:
console.log('Unknown event type:', payload.type)
}
return NextResponse.json({ received: true })
} catch (error) {
console.error('Webhook error:', error)
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
)
}
}
// Handlers
async function handleEmailSent(data: ResendWebhookPayload['data']) {
// Save status in the database
}
async function handleEmailDelivered(data: ResendWebhookPayload['data']) {
// Update status in the database
}
async function handleEmailOpened(data: ResendWebhookPayload['data']) {
// Record open event for analytics
}
async function handleEmailClicked(data: ResendWebhookPayload['data']) {
// Track link clicks
}
async function handleEmailBounced(data: ResendWebhookPayload['data']) {
// Mark email as inactive
// Remove from mailing list
}
async function handleEmailComplained(data: ResendWebhookPayload['data']) {
// Immediately remove from all lists
// Add to blacklist
}Domain management
Domain verification
import { resend } from '@/lib/resend'
// Add a domain
const { data: domain, error } = await resend.domains.create({
name: 'yourdomain.com',
region: 'eu-west-1', // or 'us-east-1'
})
// List domains
const { data: domains } = await resend.domains.list()
// Verify domain
const { data: verified } = await resend.domains.verify('domain_id')
// Domain details (DNS records)
const { data: domainDetails } = await resend.domains.get('domain_id')
console.log(domainDetails.records) // DNS records to addDNS records
After adding a domain, Resend will return DNS records to configure:
// Example response
{
records: [
{
type: 'MX',
name: 'send',
value: 'feedback-smtp.eu-west-1.amazonses.com',
priority: 10
},
{
type: 'TXT',
name: 'send',
value: 'v=spf1 include:amazonses.com ~all'
},
{
type: 'TXT',
name: 'resend._domainkey',
value: 'p=MIGfMA0GCSqGSIb3DQEBAQUAA...'
}
]
}Audiences and contacts
Managing recipient lists
import { resend } from '@/lib/resend'
// Create an audience (list)
const { data: audience } = await resend.audiences.create({
name: 'Newsletter Subscribers'
})
// Add a contact
const { data: contact } = await resend.contacts.create({
audience_id: audience.id,
email: 'user@example.com',
first_name: 'John',
last_name: 'Smith',
unsubscribed: false,
})
// Retrieve contacts
const { data: contacts } = await resend.contacts.list({
audience_id: audience.id,
})
// Update a contact
await resend.contacts.update({
audience_id: audience.id,
id: contact.id,
first_name: 'John',
})
// Remove a contact
await resend.contacts.remove({
audience_id: audience.id,
id: contact.id,
})Sending to an audience
const { data, error } = await resend.emails.send({
from: 'Newsletter <newsletter@yourdomain.com>',
to: audience.id, // Audience ID instead of a specific email
subject: 'New blog article',
react: NewsletterEmail({ title: 'Article', content: '...' }),
})Batch sending
Sending multiple emails at once
import { resend } from '@/lib/resend'
const emails = [
{
from: 'newsletter@yourdomain.com',
to: 'user1@example.com',
subject: 'Newsletter #1',
html: '<p>Content 1</p>',
},
{
from: 'newsletter@yourdomain.com',
to: 'user2@example.com',
subject: 'Newsletter #1',
html: '<p>Content 2</p>',
},
// ... more emails
]
const { data, error } = await resend.batch.send(emails)
// data contains an array with each email's ID
console.log(data) // [{ id: 'email_1' }, { id: 'email_2' }]Personalized batch sending
interface Subscriber {
email: string
name: string
preferences: string[]
}
async function sendPersonalizedNewsletter(subscribers: Subscriber[]) {
const emails = subscribers.map(subscriber => ({
from: 'newsletter@yourdomain.com',
to: subscriber.email,
subject: `Hey ${subscriber.name}! New newsletter`,
react: NewsletterEmail({
name: subscriber.name,
topics: subscriber.preferences,
}),
tags: [
{ name: 'campaign', value: 'weekly-newsletter' },
{ name: 'subscriber_id', value: subscriber.email },
],
}))
// Batch in groups of 100 emails (Resend limit)
const batches = []
for (let i = 0; i < emails.length; i += 100) {
batches.push(emails.slice(i, i + 100))
}
const results = []
for (const batch of batches) {
const { data, error } = await resend.batch.send(batch)
if (error) {
console.error('Batch error:', error)
}
results.push(...(data || []))
// Wait between batches
await new Promise(resolve => setTimeout(resolve, 1000))
}
return results
}Rate limiting and error handling
Implementing rate limiting
import { resend } from '@/lib/resend'
class EmailService {
private queue: Array<() => Promise<void>> = []
private processing = false
private rateLimit = 10 // emails per second
async send(params: Parameters<typeof resend.emails.send>[0]) {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const result = await this.sendWithRetry(params)
resolve(result)
} catch (error) {
reject(error)
}
})
this.processQueue()
})
}
private async processQueue() {
if (this.processing) return
this.processing = true
while (this.queue.length > 0) {
const batch = this.queue.splice(0, this.rateLimit)
await Promise.all(batch.map(fn => fn()))
await new Promise(resolve => setTimeout(resolve, 1000))
}
this.processing = false
}
private async sendWithRetry(
params: Parameters<typeof resend.emails.send>[0],
retries = 3
) {
for (let i = 0; i < retries; i++) {
const { data, error } = await resend.emails.send(params)
if (!error) {
return data
}
// Retry on rate limit or server error
if (error.statusCode === 429 || error.statusCode >= 500) {
const delay = Math.pow(2, i) * 1000 // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay))
continue
}
// Do not retry on other errors
throw error
}
throw new Error('Max retries exceeded')
}
}
export const emailService = new EmailService()Best practices
1. Use tags for tracking
await resend.emails.send({
// ...
tags: [
{ name: 'type', value: 'transactional' },
{ name: 'campaign', value: 'welcome-series' },
{ name: 'user_id', value: userId },
],
})2. Always validate email addresses
import { z } from 'zod'
const emailSchema = z.string().email()
async function sendEmail(to: string, subject: string, content: string) {
const validatedEmail = emailSchema.parse(to)
await resend.emails.send({
from: 'noreply@yourdomain.com',
to: validatedEmail,
subject,
html: content,
})
}3. Handle unsubscribe
// In every marketing email
<Text style={footerText}>
Don't want to receive these messages?{' '}
<Link href={`https://yourdomain.com/unsubscribe?email=${encodeURIComponent(email)}`}>
Unsubscribe
</Link>
</Text>4. Test templates before sending
// Use onboarding@resend.dev for testing
const { data, error } = await resend.emails.send({
from: 'onboarding@resend.dev', // Resend test domain
to: 'test@example.com',
subject: 'Test email',
react: TestEmail(),
})Pricing and limits
| Plan | Emails/month | Price | Limits |
|---|---|---|---|
| Free | 3,000 | $0 | 100/day |
| Pro | 50,000 | $20/mo | + $0.28/1000 over |
| Scale | 100,000 | $90/mo | + $0.25/1000 over |
| Enterprise | Custom | Custom | Dedicated infrastructure |
Rate limits
- Free: 2 emails/second
- Pro: 10 emails/second
- Scale: 50 emails/second
- Batch API: max 100 emails per request
FAQ - Frequently asked questions
Is Resend better than SendGrid?
Resend offers a better developer experience thanks to React Email and its modern API. SendGrid has more marketing features, but Resend is simpler to integrate in Next.js applications.
How do I send emails with attachments?
Use the attachments field with a Buffer or base64:
const { data } = await resend.emails.send({
// ...
attachments: [
{
filename: 'report.pdf',
content: Buffer.from(pdfData),
},
],
})Can I use my own domain right away?
Yes, but it requires DNS verification. For testing, you can use onboarding@resend.dev.
How do I track email opens?
Enable tracking in the Resend dashboard and handle the email.opened webhook.
Summary
Resend is a modern email sending solution that fits perfectly into the React and Next.js ecosystem. Key advantages:
- React Email - build templates as React components
- Type-safe API - full TypeScript support
- High deliverability - dedicated infrastructure
- Simplicity - intuitive API with no unnecessary configuration
- Webhooks - real-time status tracking
If you are building a Next.js application and need a reliable transactional email delivery system, Resend is an excellent choice.