Resend Integration
Send transactional emails with Resend and React Email.
Dependencies#
npm install resend @react-email/componentsEnvironment Variables#
# .env.local
RESEND_API_KEY=re_...
EMAIL_FROM=noreply@yourdomain.comClient Setup#
// lib/resend.ts
import { Resend } from 'resend';
export const resend = new Resend(process.env.RESEND_API_KEY);Email Templates#
Welcome Email#
1// emails/welcome.tsx
2import {
3 Body,
4 Button,
5 Container,
6 Head,
7 Heading,
8 Html,
9 Img,
10 Link,
11 Preview,
12 Section,
13 Text,
14} from '@react-email/components';
15
16interface WelcomeEmailProps {
17 name: string;
18 loginUrl: string;
19}
20
21export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
22 return (
23 <Html>
24 <Head />
25 <Preview>Welcome to our platform!</Preview>
26 <Body style={main}>
27 <Container style={container}>
28 <Img
29 src="https://yourdomain.com/logo.png"
30 width="40"
31 height="40"
32 alt="Logo"
33 />
34
35 <Heading style={h1}>Welcome, {name}!</Heading>
36
37 <Text style={text}>
38 Thanks for signing up. We're excited to have you on board.
39 </Text>
40
41 <Section style={buttonContainer}>
42 <Button style={button} href={loginUrl}>
43 Get Started
44 </Button>
45 </Section>
46
47 <Text style={text}>
48 If you have any questions, just reply to this email—we're always
49 happy to help out.
50 </Text>
51
52 <Text style={footer}>
53 © 2024 Your Company. All rights reserved.
54 </Text>
55 </Container>
56 </Body>
57 </Html>
58 );
59}
60
61const main = {
62 backgroundColor: '#f6f9fc',
63 fontFamily:
64 '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
65};
66
67const container = {
68 backgroundColor: '#ffffff',
69 margin: '0 auto',
70 padding: '40px 20px',
71 maxWidth: '560px',
72};
73
74const h1 = {
75 color: '#1a1a1a',
76 fontSize: '24px',
77 fontWeight: '600',
78 lineHeight: '40px',
79 margin: '30px 0',
80};
81
82const text = {
83 color: '#444',
84 fontSize: '16px',
85 lineHeight: '26px',
86 margin: '16px 0',
87};
88
89const buttonContainer = {
90 textAlign: 'center' as const,
91 margin: '32px 0',
92};
93
94const button = {
95 backgroundColor: '#6366f1',
96 borderRadius: '8px',
97 color: '#fff',
98 fontSize: '16px',
99 fontWeight: '600',
100 textDecoration: 'none',
101 textAlign: 'center' as const,
102 display: 'inline-block',
103 padding: '12px 24px',
104};
105
106const footer = {
107 color: '#898989',
108 fontSize: '12px',
109 marginTop: '40px',
110};
111
112export default WelcomeEmail;Password Reset Email#
1// emails/password-reset.tsx
2import {
3 Body,
4 Button,
5 Container,
6 Head,
7 Heading,
8 Html,
9 Preview,
10 Section,
11 Text,
12} from '@react-email/components';
13
14interface PasswordResetProps {
15 resetUrl: string;
16 expiresIn: string;
17}
18
19export function PasswordResetEmail({ resetUrl, expiresIn }: PasswordResetProps) {
20 return (
21 <Html>
22 <Head />
23 <Preview>Reset your password</Preview>
24 <Body style={main}>
25 <Container style={container}>
26 <Heading style={h1}>Reset Your Password</Heading>
27
28 <Text style={text}>
29 We received a request to reset your password. Click the button
30 below to create a new password.
31 </Text>
32
33 <Section style={buttonContainer}>
34 <Button style={button} href={resetUrl}>
35 Reset Password
36 </Button>
37 </Section>
38
39 <Text style={text}>
40 This link will expire in {expiresIn}. If you didn't request this,
41 you can safely ignore this email.
42 </Text>
43
44 <Text style={footer}>
45 If the button doesn't work, copy and paste this URL into your
46 browser: {resetUrl}
47 </Text>
48 </Container>
49 </Body>
50 </Html>
51 );
52}
53
54// ... styles same as above
55
56export default PasswordResetEmail;Invoice Email#
1// emails/invoice.tsx
2import {
3 Body,
4 Container,
5 Head,
6 Heading,
7 Html,
8 Preview,
9 Row,
10 Column,
11 Section,
12 Text,
13} from '@react-email/components';
14
15interface InvoiceItem {
16 description: string;
17 quantity: number;
18 price: number;
19}
20
21interface InvoiceEmailProps {
22 invoiceNumber: string;
23 customerName: string;
24 items: InvoiceItem[];
25 total: number;
26 dueDate: string;
27}
28
29export function InvoiceEmail({
30 invoiceNumber,
31 customerName,
32 items,
33 total,
34 dueDate,
35}: InvoiceEmailProps) {
36 return (
37 <Html>
38 <Head />
39 <Preview>Invoice #{invoiceNumber}</Preview>
40 <Body style={main}>
41 <Container style={container}>
42 <Heading style={h1}>Invoice #{invoiceNumber}</Heading>
43
44 <Text style={text}>Hi {customerName},</Text>
45 <Text style={text}>Here's your invoice for this billing period.</Text>
46
47 <Section style={table}>
48 <Row style={tableHeader}>
49 <Column style={tableHeaderCell}>Item</Column>
50 <Column style={tableHeaderCell}>Qty</Column>
51 <Column style={tableHeaderCell}>Price</Column>
52 </Row>
53 {items.map((item, index) => (
54 <Row key={index} style={tableRow}>
55 <Column style={tableCell}>{item.description}</Column>
56 <Column style={tableCell}>{item.quantity}</Column>
57 <Column style={tableCell}>${item.price.toFixed(2)}</Column>
58 </Row>
59 ))}
60 <Row style={totalRow}>
61 <Column style={tableCell} colSpan={2}>Total</Column>
62 <Column style={totalCell}>${total.toFixed(2)}</Column>
63 </Row>
64 </Section>
65
66 <Text style={text}>
67 Payment is due by {dueDate}.
68 </Text>
69 </Container>
70 </Body>
71 </Html>
72 );
73}
74
75const table = {
76 width: '100%',
77 borderCollapse: 'collapse' as const,
78 margin: '24px 0',
79};
80
81const tableHeader = {
82 backgroundColor: '#f6f9fc',
83};
84
85const tableHeaderCell = {
86 padding: '12px',
87 textAlign: 'left' as const,
88 fontSize: '12px',
89 fontWeight: '600',
90 color: '#666',
91 borderBottom: '1px solid #eee',
92};
93
94const tableRow = {
95 borderBottom: '1px solid #eee',
96};
97
98const tableCell = {
99 padding: '12px',
100 fontSize: '14px',
101};
102
103const totalRow = {
104 backgroundColor: '#f6f9fc',
105};
106
107const totalCell = {
108 padding: '12px',
109 fontSize: '16px',
110 fontWeight: '600',
111};
112
113export default InvoiceEmail;Email Service#
1// lib/email.ts
2import { resend } from '@/lib/resend';
3import { WelcomeEmail } from '@/emails/welcome';
4import { PasswordResetEmail } from '@/emails/password-reset';
5import { InvoiceEmail } from '@/emails/invoice';
6
7const FROM_EMAIL = process.env.EMAIL_FROM || 'noreply@example.com';
8
9export async function sendWelcomeEmail(to: string, name: string) {
10 const loginUrl = `${process.env.NEXT_PUBLIC_APP_URL}/sign-in`;
11
12 const { data, error } = await resend.emails.send({
13 from: FROM_EMAIL,
14 to,
15 subject: 'Welcome to Our Platform!',
16 react: WelcomeEmail({ name, loginUrl }),
17 });
18
19 if (error) {
20 console.error('Failed to send welcome email:', error);
21 throw error;
22 }
23
24 return data;
25}
26
27export async function sendPasswordResetEmail(
28 to: string,
29 resetToken: string
30) {
31 const resetUrl = `${process.env.NEXT_PUBLIC_APP_URL}/reset-password?token=${resetToken}`;
32
33 const { data, error } = await resend.emails.send({
34 from: FROM_EMAIL,
35 to,
36 subject: 'Reset Your Password',
37 react: PasswordResetEmail({ resetUrl, expiresIn: '1 hour' }),
38 });
39
40 if (error) {
41 console.error('Failed to send password reset email:', error);
42 throw error;
43 }
44
45 return data;
46}
47
48export async function sendInvoiceEmail(
49 to: string,
50 invoice: {
51 number: string;
52 customerName: string;
53 items: Array<{ description: string; quantity: number; price: number }>;
54 total: number;
55 dueDate: string;
56 }
57) {
58 const { data, error } = await resend.emails.send({
59 from: FROM_EMAIL,
60 to,
61 subject: `Invoice #${invoice.number}`,
62 react: InvoiceEmail({
63 invoiceNumber: invoice.number,
64 customerName: invoice.customerName,
65 items: invoice.items,
66 total: invoice.total,
67 dueDate: invoice.dueDate,
68 }),
69 });
70
71 if (error) {
72 console.error('Failed to send invoice email:', error);
73 throw error;
74 }
75
76 return data;
77}Batch Sending#
1// lib/email/batch.ts
2import { resend } from '@/lib/resend';
3
4export async function sendBatchEmails(
5 emails: Array<{
6 to: string;
7 subject: string;
8 react: React.ReactElement;
9 }>
10) {
11 const batch = emails.map((email) => ({
12 from: process.env.EMAIL_FROM!,
13 to: email.to,
14 subject: email.subject,
15 react: email.react,
16 }));
17
18 const { data, error } = await resend.batch.send(batch);
19
20 if (error) {
21 console.error('Batch send failed:', error);
22 throw error;
23 }
24
25 return data;
26}Email Preview Server#
1// scripts/preview-email.ts
2import { render } from '@react-email/render';
3import { WelcomeEmail } from '@/emails/welcome';
4import { writeFileSync } from 'fs';
5
6async function main() {
7 const html = await render(
8 WelcomeEmail({
9 name: 'John',
10 loginUrl: 'https://example.com/login',
11 })
12 );
13
14 writeFileSync('preview.html', html);
15 console.log('Preview saved to preview.html');
16}
17
18main();API Route for Sending#
1// app/api/email/send/route.ts
2import { auth } from '@clerk/nextjs/server';
3import { NextRequest, NextResponse } from 'next/server';
4import { sendWelcomeEmail } from '@/lib/email';
5
6export async function POST(request: NextRequest) {
7 const { userId } = await auth();
8 if (!userId) {
9 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
10 }
11
12 const { type, to, data } = await request.json();
13
14 try {
15 switch (type) {
16 case 'welcome':
17 await sendWelcomeEmail(to, data.name);
18 break;
19 default:
20 return NextResponse.json(
21 { error: 'Unknown email type' },
22 { status: 400 }
23 );
24 }
25
26 return NextResponse.json({ success: true });
27 } catch (error) {
28 console.error('Email send failed:', error);
29 return NextResponse.json(
30 { error: 'Failed to send email' },
31 { status: 500 }
32 );
33 }
34}Webhooks for Tracking#
1// app/api/webhooks/resend/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { prisma } from '@/lib/prisma';
4
5export async function POST(request: NextRequest) {
6 const payload = await request.json();
7
8 switch (payload.type) {
9 case 'email.delivered':
10 await prisma.emailLog.update({
11 where: { messageId: payload.data.email_id },
12 data: { status: 'delivered', deliveredAt: new Date() },
13 });
14 break;
15
16 case 'email.opened':
17 await prisma.emailLog.update({
18 where: { messageId: payload.data.email_id },
19 data: { opened: true, openedAt: new Date() },
20 });
21 break;
22
23 case 'email.clicked':
24 await prisma.emailLog.update({
25 where: { messageId: payload.data.email_id },
26 data: {
27 clicked: true,
28 clickedAt: new Date(),
29 clickedLink: payload.data.link,
30 },
31 });
32 break;
33
34 case 'email.bounced':
35 await prisma.emailLog.update({
36 where: { messageId: payload.data.email_id },
37 data: { status: 'bounced', bouncedAt: new Date() },
38 });
39 break;
40 }
41
42 return NextResponse.json({ received: true });
43}Testing#
1// __tests__/emails/welcome.test.tsx
2import { render } from '@react-email/render';
3import { WelcomeEmail } from '@/emails/welcome';
4
5describe('WelcomeEmail', () => {
6 it('renders correctly', async () => {
7 const html = await render(
8 WelcomeEmail({
9 name: 'John',
10 loginUrl: 'https://example.com/login',
11 })
12 );
13
14 expect(html).toContain('Welcome, John!');
15 expect(html).toContain('https://example.com/login');
16 });
17});