We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide35 min read

Resend

Resend is a modern email API for developers. Learn React Email, templates, webhooks, and Next.js integration.

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

  1. React Email - Twórz szablony emaili jako komponenty React
  2. Type-safe API - Pełne wsparcie TypeScript
  3. Wysoka dostarczalność - Własna infrastruktura z monitoringiem
  4. Proste API - Intuicyjne, bez zbędnej konfiguracji
  5. Webhooks - Śledzenie statusu emaili w czasie rzeczywistym
  6. Domains verification - Łatwa konfiguracja własnych domen

Instalacja i konfiguracja

Instalacja pakietów

Code
Bash
# 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-dev

Konfiguracja API Key

Utwórz konto na resend.com i wygeneruj API key:

TSlib/resend.ts
TypeScript
// 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
Bash
# .env.local
RESEND_API_KEY=re_xxxxxxxxxxxxx

Podstawowe wysyłanie emaili

Prosty email tekstowy

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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:

TSemails/welcome.tsx
TypeScript
// 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

Code
TypeScript
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
JSON
// package.json
{
  "scripts": {
    "email:dev": "email dev --dir emails --port 3001"
  }
}
Code
Bash
npm run email:dev
# Otwórz http://localhost:3001 dla podglądu szablonów

Biblioteka szablonów emaili

Email potwierdzenia zamówienia

TSemails/order-confirmation.tsx
TypeScript
// 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)}</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)}</Text></Column>
            </Row>
            <Row>
              <Column><Text style={summaryLabel}>Dostawa:</Text></Column>
              <Column><Text style={summaryValue}>{shipping.toFixed(2)}</Text></Column>
            </Row>
            <Row>
              <Column><Text style={totalLabel}>Razem:</Text></Column>
              <Column><Text style={totalValue}>{total.toFixed(2)}</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

TSemails/password-reset.tsx
TypeScript
// 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)

TSapp/api/send-email/route.ts
TypeScript
// 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

TSapp/actions/email.ts
TypeScript
// 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

TScomponents/ContactForm.tsx
TypeScript
// 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łany
  • email.delivered - Email został dostarczony
  • email.opened - Email został otwarty
  • email.clicked - Link w emailu został kliknięty
  • email.bounced - Email odbił się
  • email.complained - Użytkownik oznaczył email jako spam

Endpoint webhooks

TSapp/api/webhooks/resend/route.ts
TypeScript
// 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

Code
TypeScript
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 dodania

Rekordy DNS

Po dodaniu domeny, Resend zwróci rekordy DNS do skonfigurowania:

Code
TypeScript
// 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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
await resend.emails.send({
  // ...
  tags: [
    { name: 'type', value: 'transactional' },
    { name: 'campaign', value: 'welcome-series' },
    { name: 'user_id', value: userId },
  ],
})

2. Zawsze waliduj adresy email

Code
TypeScript
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

Code
TypeScript
// 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ą

Code
TypeScript
// 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

PlanEmaile/miesiącCenaLimity
Free3,000$0100/dzień
Pro50,000$20/mo+ $0.28/1000 over
Scale100,000$90/mo+ $0.25/1000 over
EnterpriseCustomCustomDedykowana 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:

Code
TypeScript
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

  1. React Email - Build email templates as React components
  2. Type-safe API - Full TypeScript support
  3. High deliverability - Dedicated infrastructure with monitoring
  4. Simple API - Intuitive, no unnecessary configuration
  5. Webhooks - Real-time email status tracking
  6. Domain verification - Easy custom domain setup

Installation and configuration

Package installation

Code
Bash
# 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-dev

API key configuration

Create an account at resend.com and generate an API key:

TSlib/resend.ts
TypeScript
// 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
Bash
# .env.local
RESEND_API_KEY=re_xxxxxxxxxxxxx

Basic email sending

Simple text email

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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:

TSemails/welcome.tsx
TypeScript
// 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

Code
TypeScript
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
JSON
// package.json
{
  "scripts": {
    "email:dev": "email dev --dir emails --port 3001"
  }
}
Code
Bash
npm run email:dev
# Open http://localhost:3001 to preview templates

Email template library

Order confirmation email

TSemails/order-confirmation.tsx
TypeScript
// 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

TSemails/password-reset.tsx
TypeScript
// 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)

TSapp/api/send-email/route.ts
TypeScript
// 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

TSapp/actions/email.ts
TypeScript
// 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

TScomponents/ContactForm.tsx
TypeScript
// 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 sent
  • email.delivered - Email has been delivered
  • email.opened - Email has been opened
  • email.clicked - A link in the email has been clicked
  • email.bounced - Email has bounced
  • email.complained - User marked the email as spam

Webhook endpoint

TSapp/api/webhooks/resend/route.ts
TypeScript
// 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

Code
TypeScript
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 add

DNS records

After adding a domain, Resend will return DNS records to configure:

Code
TypeScript
// 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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
await resend.emails.send({
  // ...
  tags: [
    { name: 'type', value: 'transactional' },
    { name: 'campaign', value: 'welcome-series' },
    { name: 'user_id', value: userId },
  ],
})

2. Always validate email addresses

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
// 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

PlanEmails/monthPriceLimits
Free3,000$0100/day
Pro50,000$20/mo+ $0.28/1000 over
Scale100,000$90/mo+ $0.25/1000 over
EnterpriseCustomCustomDedicated 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:

Code
TypeScript
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.