Next.js ile Resend + React Email: Transaksiyonel E-posta Entegrasyonu Rehberi
Next.js 16 App Router ile Resend ve React Email kullanarak transaksiyonel e-posta sistemi kurma rehberi: domain doğrulama, React şablonları, Better Auth magic link entegrasyonu ve KVKK uyumu.

Önemli Not: Bu yazıdaki teknik bilgiler yazım tarihi itibarıyla geçerlidir. Kullanılan kütüphaneler, API'ler ve servisler zaman içinde değişebilir. Ücretlendirme, yasal düzenleme ve vergi konularında ilgili resmi kaynakları ve uzmanları referans alınız. Bu içerik bilgilendirme amaçlı olup herhangi bir finansal veya hukuki tavsiye niteliği taşımamaktadır.
Kullanıcı kaydı tamamlandı — hoşgeldiniz e-postası gitmeli. Müşteri sipariş verdi — onay e-postası gitmeli. Şifre sıfırlama istendi — magic link gitmeli. Bu e-postaların güvenilir, hızlı ve düzgün görünmesi uygulamanızın kalite algısını doğrudan etkiler.
Bu rehberde Next.js 16 App Router ile Resend ve React Email kullanarak kurumsal düzeyde transaksiyonel e-posta sistemi kuracağız. Bu projenin kendisinde de Resend aktif olarak kullanılıyor — müşteri portalı giriş akışı için Better Auth magic link e-postaları Resend üzerinden gönderiliyor.
2026'da Transaksiyonel E-posta Seçenekleri
Transaksiyonel e-posta için birkaç seçenek mevcut:
| Servis | Öne Çıkan Özellik | Developer Deneyimi |
|---|---|---|
| Resend | React Email desteği, modern API | ⭐⭐⭐⭐⭐ |
| Postmark | Yüksek deliverability, e-posta log | ⭐⭐⭐⭐ |
| SendGrid | Ölçeklenebilir, marketing + transaksiyonel | ⭐⭐⭐ |
| Amazon SES | Düşük maliyet, AWS ekosistemi | ⭐⭐ (kurulum karmaşık) |
| Mailgun | Güçlü API, log sistemi | ⭐⭐⭐ |
Neden Resend?
Resend, developer deneyimini merkeze alan modern bir transaksiyonel e-posta servisi. Next.js ekibiyle yakın çalışarak tasarlanmış — App Router, Server Actions ve React Email ile sorunsuz entegrasyon sunar.
Öne çıkan avantajlar:
- React Email desteği: E-posta şablonlarını React component olarak yazma imkânı
- TypeScript-first API: Tam tip güvenliği, otomatik tamamlama
- Ücretsiz başlangıç katmanı: Küçük projeler ve geliştirme için yeterli (güncel limitler için resend.com adresini ziyaret edin)
- Gerçek zamanlı önizleme:
email devkomutuyla lokal tarayıcıda şablon testi - Modern dashboard: Delivery tracking, hata logları, webhook yönetimi
Resend Hesabı ve Domain Doğrulama
Hesap Açma
- resend.com adresine gidin ve GitHub veya e-posta ile kayıt olun
- Dashboard'a giriş yapın
- API Keys bölümünden yeni API anahtarı oluşturun
- Anahtarı
.env.localdosyanıza ekleyin:
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
Domain Doğrulama (DNS Kayıtları)
Production ortamında kendi domain'inizden e-posta göndermek için domain doğrulaması zorunludur. Bu adım deliverability (teslim edilebilirlik) için kritik — doğrulanmamış domain'den gelen e-postalar spam klasörüne düşer.
Dashboard → Domains → Add Domain adımlarını izleyin. Resend size iki tür DNS kaydı eklemenizi söyler:
SPF kaydı (TXT record):
Tip: TXT
İsim: @ (veya domain adınız)
Değer: "v=spf1 include:amazonses.com ~all"
DKIM kayıtları (CNAME records):
Tip: CNAME
İsim: resend._domainkey
Değer: resend._domainkey.resend.dev
Tam DNS değerleri Resend dashboard'dan alınır — her hesap için farklı değerler üretilir. DNS yayılması genellikle birkaç dakika sürer, en fazla 24-48 saate kadar uzayabilir.
İpucu: DNS kayıtlarını ekledikten sonra dashboard'dan "Verify DNS Records" butonuna tıklayın. Doğrulama başarılıysa domain'in yanında yeşil onay işareti görünür.
Kurulum
Gerekli paketleri yükleyin:
bun add resend @react-email/components @react-email/render
Paket açıklamaları:
resend— Resend API istemcisi@react-email/components— E-posta için özel HTML React component'leri@react-email/render— React component'ini e-posta uyumlu HTML string'e dönüştürme
Merkezi Resend İstemcisi
Uygulamanın her yerinde kullanmak için tek bir resend instance'ı oluşturun:
// lib/email/resend.ts
import { Resend } from 'resend'
import { env } from '@/lib/env'
export const resend = new Resend(env.RESEND_API_KEY)
İlk E-posta: Route Handler ile Gönderim
Next.js 16 App Router'da e-posta göndermek için Route Handler kullanın. E-posta gönderimi her zaman sunucu tarafında gerçekleşmeli — API anahtarınız asla client bundle'a dahil edilmemeli.
// app/api/email/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { resend } from '@/lib/email/resend'
export async function POST(request: NextRequest) {
const { to, name } = await request.json()
const { data, error } = await resend.emails.send({
from: 'Şirketiniz <noreply@example.com>',
to: [to],
subject: 'Hoş Geldiniz!',
html: `<p>Merhaba <strong>${name}</strong>, aramıza hoş geldiniz!</p>`,
})
if (error) {
return NextResponse.json({ error }, { status: 400 })
}
return NextResponse.json({ id: data?.id })
}
resend.emails.send() Parametreleri
const { data, error } = await resend.emails.send({
// Zorunlu parametreler
from: 'Gönderici Adı <noreply@domain.com>', // doğrulanmış domain
to: ['alici@example.com'], // dizi, max 50 alıcı
subject: 'E-posta konusu',
// İçerik — html veya react'tan biri zorunlu
html: '<h1>HTML içerik</h1>',
// veya:
react: <EmailTemplate prop="değer" />,
// Opsiyonel parametreler
cc: ['cc@example.com'],
bcc: ['bcc@example.com'],
reply_to: 'destek@example.com',
scheduled_at: '2026-04-01T10:00:00Z', // zamanlanmış gönderim
attachments: [
{
filename: 'fatura.pdf',
content: pdfBuffer, // Buffer veya base64 string
},
],
tags: [
{ name: 'category', value: 'welcome-email' }, // dashboard filtreleme için
],
})
Server Actions ile Form Submit → E-posta
React'ın Server Actions özelliğiyle form gönderimlerinde doğrudan e-posta tetikleyebilirsiniz:
// app/iletisim/actions.ts
'use server'
import { resend } from '@/lib/email/resend'
import { z } from 'zod'
const contactSchema = z.object({
name: z.string().min(2, 'Ad en az 2 karakter olmalı'),
email: z.string().email('Geçerli bir e-posta girin'),
message: z.string().min(10, 'Mesaj en az 10 karakter olmalı'),
})
export async function sendContactEmail(formData: FormData) {
const parsed = contactSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
})
if (!parsed.success) {
return { error: 'Geçersiz form verisi.' }
}
const { name, email, message } = parsed.data
const { error } = await resend.emails.send({
from: 'İletişim Formu <noreply@example.com>',
to: ['info@example.com'],
reply_to: email,
subject: `Yeni İletişim Mesajı: ${name}`,
html: `
<h2>Yeni Mesaj</h2>
<p><strong>Ad:</strong> ${name}</p>
<p><strong>E-posta:</strong> ${email}</p>
<p><strong>Mesaj:</strong></p>
<p>${message}</p>
`,
})
if (error) {
return { error: 'E-posta gönderilemedi. Lütfen tekrar deneyin.' }
}
return { success: true }
}
React Email Şablonları
HTML string ile e-posta yazmak hızla karmaşık ve bakımı zor hale gelir. React Email, e-posta şablonlarını React component olarak yazmanızı sağlar — IDE desteği, tip güvenliği ve canlı önizleme dahil.
Lokal Önizleme Sunucusu
package.json'a script ekleyin:
{
"scripts": {
"email": "email dev --dir emails --port 3001"
}
}
bun run email
# http://localhost:3001 adresinde e-posta önizlemesi açılır
Sipariş Onay Şablonu
// emails/order-confirmation.tsx
import {
Body,
Button,
Container,
Head,
Heading,
Hr,
Html,
Preview,
Section,
Text,
} from '@react-email/components'
interface OrderConfirmationEmailProps {
customerName: string
orderNumber: string
orderDate: string
totalAmount: string
items: Array<{ name: string; quantity: number; price: string }>
trackingUrl: string
}
export default function OrderConfirmationEmail({
customerName,
orderNumber,
orderDate,
totalAmount,
items,
trackingUrl,
}: OrderConfirmationEmailProps) {
return (
<Html lang="tr">
<Head />
<Preview>Sipariş #{orderNumber} onaylandı — Teşekkürler!</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Siparişiniz Alındı ✓</Heading>
<Text style={text}>Merhaba {customerName},</Text>
<Text style={text}>
{orderDate} tarihli siparişiniz başarıyla alındı.
Sipariş numaranız: <strong>#{orderNumber}</strong>
</Text>
<Section style={orderSection}>
<Heading as="h2" style={h2}>
Sipariş Detayları
</Heading>
{items.map((item, index) => (
<Text key={index} style={itemText}>
{item.name} × {item.quantity} — {item.price}
</Text>
))}
<Hr style={hr} />
<Text style={totalText}>
Toplam: <strong>{totalAmount}</strong>
</Text>
</Section>
<Button href={trackingUrl} style={button}>
Siparişi Takip Et
</Button>
<Text style={footer}>
Sorularınız için destek@example.com adresimize yazabilirsiniz.
</Text>
</Container>
</Body>
</Html>
)
}
// Inline stiller — e-posta istemcisi uyumluluğu için inline stil zorunlu
const main = {
backgroundColor: '#f6f9fc',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
}
const container = {
backgroundColor: '#ffffff',
margin: '0 auto',
padding: '24px',
maxWidth: '600px',
}
const h1 = { color: '#1a1a1a', fontSize: '24px', fontWeight: 'bold' }
const h2 = { color: '#333', fontSize: '18px' }
const text = { color: '#444', fontSize: '16px', lineHeight: '1.6' }
const orderSection = {
backgroundColor: '#f4f4f5',
borderRadius: '8px',
padding: '16px',
}
const itemText = { color: '#333', fontSize: '14px', margin: '4px 0' }
const hr = { borderColor: '#e1e1e1', margin: '12px 0' }
const totalText = { color: '#1a1a1a', fontSize: '16px' }
const button = {
backgroundColor: '#FFB800',
borderRadius: '6px',
color: '#000',
fontSize: '16px',
fontWeight: 'bold',
padding: '12px 24px',
textDecoration: 'none',
display: 'inline-block',
margin: '16px 0',
}
const footer = { color: '#888', fontSize: '13px', marginTop: '24px' }
render() Fonksiyonu ile HTML Üretme
React Email component'lerinizi render() fonksiyonu ile HTML string'e dönüştürün:
// lib/email/send-order-confirmation.ts
import { render } from '@react-email/render'
import OrderConfirmationEmail from '@/emails/order-confirmation'
import { resend } from './resend'
export async function sendOrderConfirmationEmail(params: {
to: string
customerName: string
orderNumber: string
orderDate: string
totalAmount: string
items: Array<{ name: string; quantity: number; price: string }>
trackingUrl: string
}) {
const html = await render(
OrderConfirmationEmail({
customerName: params.customerName,
orderNumber: params.orderNumber,
orderDate: params.orderDate,
totalAmount: params.totalAmount,
items: params.items,
trackingUrl: params.trackingUrl,
}),
)
return resend.emails.send({
from: 'Siparişleriniz <siparis@example.com>',
to: [params.to],
subject: `Sipariş #${params.orderNumber} Onaylandı`,
html,
tags: [{ name: 'category', value: 'order-confirmation' }],
})
}
Türkçe Karakter Desteği
Resend REST API UTF-8 kodlamasını tam olarak destekler. Türkçe karakterler (ı, ş, ç, ü, ö, ğ) hem konu satırında hem içerikte sorunsuz çalışır — ek encoding veya kaçış karakteri gerekmez:
// Tüm Türkçe karakterler sorunsuz çalışır
await resend.emails.send({
from: 'Şirketiniz <noreply@example.com>',
to: ['kullanici@example.com'],
subject: 'Siparişiniz onaylandı — Teşekkürler! 🎉',
html: '<p>Merhaba! Siparişiniz hazırlanıyor, kısa sürede kargoya verilecek.</p>',
})
React Email de <Head /> component'i aracılığıyla doğru charset (UTF-8) meta etiketini otomatik olarak ekler.
Better Auth ile Magic Link Akışı
Bu projede müşteri portalı girişi için Better Auth + Resend kombinasyonu kullanılıyor. Kullanıcı e-posta adresini giriyor, sistemin gönderdiği magic link ile şifresiz oturum açıyor.
auth.ts Konfigürasyonu
// lib/auth.ts
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { magicLink } from 'better-auth/plugins'
import { render } from '@react-email/render'
import { db } from '@/lib/db'
import { resend } from '@/lib/email/resend'
import MagicLinkEmail from '@/emails/magic-link'
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'pg' }),
plugins: [
magicLink({
sendMagicLink: async ({ email, url }) => {
const html = await render(MagicLinkEmail({ url }))
const { error } = await resend.emails.send({
from: 'Müşteri Portalı <portal@example.com>',
to: [email],
subject: 'Giriş Linkiniz',
html,
})
if (error) {
throw new Error('Magic link e-postası gönderilemedi.')
}
},
expiresIn: 300, // 5 dakika (saniye cinsinden)
allowedAttempts: 1,
}),
],
})
Magic Link E-posta Şablonu
// emails/magic-link.tsx
import {
Body,
Button,
Container,
Head,
Html,
Preview,
Text,
} from '@react-email/components'
interface MagicLinkEmailProps {
url: string
}
export default function MagicLinkEmail({ url }: MagicLinkEmailProps) {
return (
<Html lang="tr">
<Head />
<Preview>Giriş linkiniz — 5 dakika geçerli</Preview>
<Body style={{ backgroundColor: '#f6f9fc', fontFamily: 'sans-serif' }}>
<Container
style={{
backgroundColor: '#fff',
margin: '0 auto',
padding: '32px',
maxWidth: '480px',
}}
>
<Text
style={{ fontSize: '22px', fontWeight: 'bold', color: '#111' }}
>
Müşteri Paneline Giriş
</Text>
<Text style={{ color: '#555', lineHeight: '1.6' }}>
Aşağıdaki butona tıklayarak müşteri panelinize erişebilirsiniz.
Bu link <strong>5 dakika</strong> geçerlidir ve yalnızca bir kez
kullanılabilir.
</Text>
<Button
href={url}
style={{
backgroundColor: '#FFB800',
color: '#000',
fontWeight: 'bold',
padding: '12px 32px',
borderRadius: '6px',
textDecoration: 'none',
display: 'inline-block',
marginTop: '16px',
}}
>
Giriş Yap →
</Button>
<Text
style={{ color: '#999', fontSize: '13px', marginTop: '24px' }}
>
Bu e-postayı siz istemediyseniz dikkate almayınız. Hesabınız
güvendedir.
</Text>
</Container>
</Body>
</Html>
)
}
KVKK E-posta Onayı
Türkiye'de KVKK (Kişisel Verilerin Korunması Kanunu) kapsamında e-posta gönderiminde bazı yükümlülükler bulunur.
Transaksiyonel e-postalar (sipariş onayı, fatura, magic link, şifre sıfırlama) sözleşmesel yükümlülük kapsamında değerlendirilir ve genellikle ayrı onay gerektirmez.
Pazarlama e-postaları (bülten, kampanya, promosyon) için açık rıza zorunludur. Bunu veritabanında kaydedin:
// Kayıt formunda KVKK onay alanı (Drizzle ORM ile)
await db.insert(users).values({
email: parsed.data.email,
emailMarketingConsent: parsed.data.emailConsent,
emailConsentDate: parsed.data.emailConsent ? new Date() : null,
})
Her pazarlama e-postasına eklemeniz gereken zorunlu bilgiler:
- Gönderen şirket adı ve adresi
- Abonelikten çıkma (unsubscribe) bağlantısı
- KVKK aydınlatma metnine bağlantı
KVKK Uyarısı: E-posta pazarlaması ve kişisel veri işleme konusundaki yasal yükümlülüklerinizi belirlemek için bir hukuk uzmanına danışın. Bu rehber teknik bilgi amaçlıdır, hukuki tavsiye niteliği taşımaz.
Webhook ile Delivery Tracking
Resend, e-posta olaylarını takip etmek için webhook desteği sunar. Hangi e-postaların iletildiğini, bounce ettiğini veya spam şikâyetiyle karşılaştığını gerçek zamanlı olarak izleyebilirsiniz.
Webhook Endpoint
// app/api/webhooks/resend/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { Webhook } from 'svix'
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
import { eq } from 'drizzle-orm'
interface ResendWebhookEvent {
type:
| 'email.sent'
| 'email.delivered'
| 'email.delivery_delayed'
| 'email.complained'
| 'email.bounced'
| 'email.opened'
| 'email.clicked'
data: {
email_id: string
from: string
to: string[]
subject: string
created_at: string
}
}
export async function POST(request: NextRequest) {
const svixId = request.headers.get('svix-id')
const svixTimestamp = request.headers.get('svix-timestamp')
const svixSignature = request.headers.get('svix-signature')
if (!svixId || !svixTimestamp || !svixSignature) {
return NextResponse.json(
{ error: 'Eksik webhook başlıkları' },
{ status: 400 },
)
}
const rawBody = await request.text()
// İmza doğrulama
const wh = new Webhook(process.env.RESEND_WEBHOOK_SECRET!)
let event: ResendWebhookEvent
try {
event = wh.verify(rawBody, {
'svix-id': svixId,
'svix-timestamp': svixTimestamp,
'svix-signature': svixSignature,
}) as ResendWebhookEvent
} catch {
return NextResponse.json({ error: 'Geçersiz imza' }, { status: 400 })
}
// Olay türüne göre işlem
switch (event.type) {
case 'email.bounced':
// E-posta adresi geçersiz — kullanıcıyı işaretle
await db
.update(users)
.set({ emailBounced: true })
.where(eq(users.email, event.data.to[0]))
break
case 'email.complained':
// Spam şikâyeti — pazarlama listesinden çıkar
await db
.update(users)
.set({ emailMarketingConsent: false })
.where(eq(users.email, event.data.to[0]))
break
}
return NextResponse.json({ received: true })
}
Webhook için svix paketini kurun:
bun add svix
Dashboard'dan Webhook Kurulumu
- Resend Dashboard → Webhooks → Add Endpoint
- Endpoint URL:
https://yourdomain.com/api/webhooks/resend - İzlenecek olayları seçin:
email.delivered,email.bounced,email.complained - Signing Secret değerini kopyalayın →
.envdosyanıza ekleyin:
RESEND_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxx
Plan Seçimi
Resend ücretsiz bir başlangıç katmanıyla projelerinizi hayata geçirmenize olanak tanır. Büyüyen uygulamalar için ücretli planlara geçiş gerekir. Güncel plan detayları ve fiyatlandırma için resend.com adresini ziyaret edin.
Sıkça Sorulan Sorular
Domain doğrulama zorunlu mu?
Production ortamı için şiddetle önerilir. Doğrulanmamış domain'den gönderilen e-postalar spam klasörüne düşebilir veya büyük e-posta servis sağlayıcıları tarafından reddedilebilir. Geliştirme sırasında Resend dashboard'undaki test adreslere onaylanan domainsiz de gönderim yapabilirsiniz.
html ve react parametrelerini aynı anda kullanabilir miyim?
Hayır. Birini seçmelisiniz. react parametresi verildiğinde Resend, React component'ini otomatik HTML'e dönüştürür. @react-email/render ile kendiniz HTML üretip html parametresiyle göndermek de eşdeğer sonuç verir — ikinci yöntem sunucu tarafında daha fazla kontrol sağlar.
Resend Türkçe karakterlerle (ı, ş, ç, ğ) sorun yaşatır mı?
Hayır. Resend API'si UTF-8 destekler ve tüm Türkçe karakterler hem konu satırında hem de e-posta içeriğinde sorunsuz çalışır. React Email de doğru charset="UTF-8" meta etiketini otomatik ekler.
Magic link e-postası spam klasörüne düşüyor, ne yapmalıyım?
Önce domain doğrulamasını tamamlayın (SPF + DKIM kayıtları). Ardından e-posta içeriğini gözden geçirin: spam tetikleyici kelimelerden kaçının, açık bir gönderici adı ve şirket bilgisi ekleyin. Ek olarak DMARC DNS kaydı eklemek deliverability'yi belirgin şekilde artırır. Resend dashboard'undaki Activity Log'dan bounce ve complaint oranlarını takip edin.
E-postayı belirli bir saatte göndermek için ne yapmalıyım?
scheduled_at parametresini ISO 8601 formatında kullanın:
await resend.emails.send({
from: 'noreply@example.com',
to: ['user@example.com'],
subject: 'Hatırlatma',
html: '<p>...</p>',
scheduled_at: '2026-04-15T09:00:00Z',
})
Zamanlanmış e-postayı resend.emails.cancel(emailId) ile iptal edebilirsiniz.
Webhook imzasını doğrulamak şart mı?
Evet, production'da kesinlikle şart. İmzasız webhook endpoint'iniz herhangi biri tarafından sahte verilerle tetiklenebilir. Resend, Svix altyapısını kullandığından svix npm paketini kurarak imzayı güvenli biçimde doğrulayabilirsiniz.
Sonuç
Resend + React Email kombinasyonu, Next.js projeleri için en developer-friendly transaksiyonel e-posta çözümü. TypeScript desteği, React ile şablon yazma imkânı ve delivery tracking özellikleriyle küçük side projelerden production ölçeğine kadar kullanışlı.
Bu projede de Resend aktif olarak kullanılıyor — müşteri portalı giriş akışı için Better Auth magic link e-postaları Resend üzerinden gönderiliyor.
Sonraki adımlar:
- Next.js ile Better Auth Kimlik Doğrulama Rehberi — magic link akışının tamamı ve proxy.ts entegrasyonu
- Modern Next.js Stack Rehberi 2026 — Next.js 16 + Drizzle ORM + Better Auth + Resend tam kurulum
Önemli Not: Bu yazıdaki teknik bilgiler yazım tarihi itibarıyla geçerlidir. Kullanılan kütüphaneler, API'ler ve servisler zaman içinde değişebilir. Ücretlendirme, yasal düzenleme ve vergi konularında ilgili resmi kaynakları ve uzmanları referans alınız. Bu içerik bilgilendirme amaçlı olup herhangi bir finansal veya hukuki tavsiye niteliği taşımamaktadır.

