CraftJS uses Resend with React Email for beautiful, type-safe transactional emails.
Features
- ✅ React Components - Write emails as React components
- ✅ Type Safety - Full TypeScript support
- ✅ Previews - Preview emails in development
- ✅ Templates - Pre-built email templates
- ✅ Deliverability - High deliverability with Resend
Configuration
Environment Variables
RESEND_API_KEY="re_..."
RESEND_FROM_EMAIL="noreply@yourdomain.com"Sign up at Resend and verify your domain for production use.
Email Client
Configure the client in src/lib/email/client.ts:
import { Resend } from "resend"
import { env } from "@/env"
export const resend = new Resend(env.RESEND_API_KEY)
export const fromEmail = env.RESEND_FROM_EMAILCreating Email Templates
Basic Template
// lib/email/templates/welcome.tsx
import {
Html,
Head,
Body,
Container,
Section,
Text,
Button,
Img,
Link,
Hr,
} from "@react-email/components"
interface WelcomeEmailProps {
name: string
loginUrl: string
}
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Img
src="https://yourapp.com/logo.png"
width="48"
height="48"
alt="CraftJS"
/>
<Text style={title}>Welcome to CraftJS!</Text>
<Text style={text}>
Hi {name},
</Text>
<Text style={text}>
Thanks for signing up! We're excited to have you on board.
Get started by exploring your dashboard.
</Text>
<Section style={buttonContainer}>
<Button style={button} href={loginUrl}>
Go to Dashboard
</Button>
</Section>
<Hr style={hr} />
<Text style={footer}>
If you didn't create an account, you can safely ignore this email.
</Text>
</Container>
</Body>
</Html>
)
}
// Styles
const main = {
backgroundColor: "#f6f9fc",
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}
const container = {
backgroundColor: "#ffffff",
margin: "0 auto",
padding: "40px 20px",
maxWidth: "560px",
borderRadius: "8px",
}
const title = {
fontSize: "24px",
fontWeight: "bold",
color: "#1a1a1a",
marginTop: "24px",
}
const text = {
fontSize: "16px",
color: "#4a4a4a",
lineHeight: "24px",
}
const buttonContainer = {
textAlign: "center" as const,
marginTop: "24px",
}
const button = {
backgroundColor: "#2563eb",
color: "#ffffff",
padding: "12px 24px",
borderRadius: "6px",
textDecoration: "none",
fontWeight: "500",
}
const hr = {
borderColor: "#e6e6e6",
marginTop: "32px",
}
const footer = {
fontSize: "12px",
color: "#8898aa",
marginTop: "24px",
}Verification Email
// lib/email/templates/verification.tsx
import {
Html,
Head,
Body,
Container,
Section,
Text,
Button,
Code,
} from "@react-email/components"
interface VerificationEmailProps {
verificationUrl: string
code?: string
}
export function VerificationEmail({ verificationUrl, code }: VerificationEmailProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Text style={title}>Verify your email</Text>
<Text style={text}>
Click the button below to verify your email address.
</Text>
<Section style={buttonContainer}>
<Button style={button} href={verificationUrl}>
Verify Email
</Button>
</Section>
{code && (
<>
<Text style={text}>
Or enter this code manually:
</Text>
<Code style={codeStyle}>{code}</Code>
</>
)}
<Text style={footer}>
This link will expire in 24 hours.
</Text>
</Container>
</Body>
</Html>
)
}
const codeStyle = {
display: "inline-block",
padding: "16px 32px",
fontSize: "32px",
fontWeight: "bold",
backgroundColor: "#f4f4f5",
borderRadius: "8px",
letterSpacing: "4px",
}
// ... other styles same as abovePassword Reset Email
// lib/email/templates/password-reset.tsx
import {
Html,
Head,
Body,
Container,
Text,
Button,
Section,
} from "@react-email/components"
interface PasswordResetEmailProps {
name: string
resetUrl: string
}
export function PasswordResetEmail({ name, resetUrl }: PasswordResetEmailProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Text style={title}>Reset your password</Text>
<Text style={text}>
Hi {name},
</Text>
<Text style={text}>
We received a request to reset your password. Click the button
below to choose a new password.
</Text>
<Section style={buttonContainer}>
<Button style={button} href={resetUrl}>
Reset Password
</Button>
</Section>
<Text style={text}>
If you didn't request a password reset, you can safely ignore
this email. Your password will remain unchanged.
</Text>
<Text style={footer}>
This link will expire in 1 hour.
</Text>
</Container>
</Body>
</Html>
)
}Sending Emails
Basic Send
import { resend, fromEmail } from "@/lib/email/client"
import { WelcomeEmail } from "@/lib/email/templates/welcome"
await resend.emails.send({
from: fromEmail,
to: "user@example.com",
subject: "Welcome to CraftJS!",
react: WelcomeEmail({
name: "John",
loginUrl: "https://yourapp.com/login",
}),
})Send with Attachments
await resend.emails.send({
from: fromEmail,
to: "user@example.com",
subject: "Your invoice",
react: InvoiceEmail({ ... }),
attachments: [
{
filename: "invoice.pdf",
content: pdfBuffer,
},
],
})Send to Multiple Recipients
// CC and BCC
await resend.emails.send({
from: fromEmail,
to: "user@example.com",
cc: ["cc@example.com"],
bcc: ["bcc@example.com"],
subject: "Team update",
react: TeamUpdateEmail({ ... }),
})
// Batch send
const emails = users.map((user) => ({
from: fromEmail,
to: user.email,
subject: "Newsletter",
react: NewsletterEmail({ name: user.name }),
}))
await resend.batch.send(emails)Email Functions
Create helper functions in src/lib/email/send.ts:
import { resend, fromEmail } from "./client"
import { WelcomeEmail } from "./templates/welcome"
import { VerificationEmail } from "./templates/verification"
import { PasswordResetEmail } from "./templates/password-reset"
export async function sendWelcomeEmail(
to: string,
name: string,
loginUrl: string
) {
return resend.emails.send({
from: `CraftJS <${fromEmail}>`,
to,
subject: "Welcome to CraftJS!",
react: WelcomeEmail({ name, loginUrl }),
})
}
export async function sendVerificationEmail(
to: string,
verificationUrl: string
) {
return resend.emails.send({
from: `CraftJS <${fromEmail}>`,
to,
subject: "Verify your email",
react: VerificationEmail({ verificationUrl }),
})
}
export async function sendPasswordResetEmail(
to: string,
name: string,
resetUrl: string
) {
return resend.emails.send({
from: `CraftJS <${fromEmail}>`,
to,
subject: "Reset your password",
react: PasswordResetEmail({ name, resetUrl }),
})
}Preview Emails
Development Preview
Create an API route to preview emails:
// app/api/email/preview/route.ts
import { NextResponse } from "next/server"
import { render } from "@react-email/render"
import { WelcomeEmail } from "@/lib/email/templates/welcome"
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const template = searchParams.get("template")
let html = ""
switch (template) {
case "welcome":
html = await render(
WelcomeEmail({
name: "John Doe",
loginUrl: "https://example.com/login",
})
)
break
default:
return NextResponse.json({ error: "Template not found" }, { status: 404 })
}
return new Response(html, {
headers: { "Content-Type": "text/html" },
})
}Visit /api/email/preview?template=welcome to preview.
Using React Email CLI
# Install React Email CLI
pnpm add -D react-email
# Start email preview server
pnpm email devThis starts a preview server at http://localhost:3001.
Integration with Auth
Connect email sending to Better Auth:
// lib/auth/server.ts
import { sendVerificationEmail, sendPasswordResetEmail } from "@/lib/email/send"
export const auth = betterAuth({
// ...
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendVerificationEmail: async ({ user, url }) => {
await sendVerificationEmail(user.email, url)
},
sendResetPasswordEmail: async ({ user, url }) => {
await sendPasswordResetEmail(user.email, user.name ?? "User", url)
},
},
})Background Email Sending
For better performance, send emails via background jobs:
// trigger/tasks.ts
import { task } from "@trigger.dev/sdk/v3"
import { sendWelcomeEmail } from "@/lib/email/send"
export const sendWelcomeEmailTask = task({
id: "send-welcome-email",
run: async (payload: { to: string; name: string; loginUrl: string }) => {
await sendWelcomeEmail(payload.to, payload.name, payload.loginUrl)
},
})
// Usage
await sendWelcomeEmailTask.trigger({
to: user.email,
name: user.name,
loginUrl: "https://yourapp.com/login",
})Error Handling
import { ResendError } from "resend"
try {
await resend.emails.send({
from: fromEmail,
to: "user@example.com",
subject: "Test",
react: TestEmail(),
})
} catch (error) {
if (error instanceof ResendError) {
console.error("Resend error:", error.message)
if (error.statusCode === 429) {
// Rate limited - retry later
}
}
throw error
}Best Practices
Tips for production email:
- Verify your domain - Required for production sending
- Use a subdomain - e.g.,
mail.yourdomain.com - Set up DKIM/SPF - Improves deliverability
- Test with real emails - Use a test domain
- Handle bounces - Set up webhook for bounce notifications
- Include unsubscribe links - Required for marketing emails
Next Steps
- Background Jobs - Async email sending
- Authentication - Auth email integration
- Webhook Handling - Handle email events
Last updated on