Add an Email Template
Create a React Email template, add a typed send helper in lib/resend/send.ts, trigger it from a Server Action or webhook.
What you are building
A new transactional email template (e.g., team invitation, weekly digest, password-change notification). The template is a React component, the send helper is a typed function in lib/resend/send.ts, and the trigger is a Server Action or webhook handler.
Step 1: create the template
Templates live in emails/. Use React Email components for cross-client compatibility (Gmail, Outlook, Apple Mail, mobile clients). Standard HTML and CSS often render inconsistently in email clients; React Email components are pre-tested.
// emails/team-invite.tsx
import {
Html,
Head,
Body,
Container,
Heading,
Text,
Button,
Hr,
} from '@react-email/components'
interface TeamInviteProps {
inviterName: string
teamName: string
acceptUrl: string
}
export default function TeamInvite({
inviterName,
teamName,
acceptUrl,
}: TeamInviteProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'system-ui, sans-serif', padding: '20px' }}>
<Container style={{ maxWidth: '600px' }}>
<Heading>You are invited to {teamName}</Heading>
<Text>
{inviterName} invited you to join the {teamName} workspace on
SecureStartKit.
</Text>
<Button
href={acceptUrl}
style={{
backgroundColor: '#6366f1',
color: '#ffffff',
padding: '12px 24px',
borderRadius: '6px',
textDecoration: 'none',
}}
>
Accept invitation
</Button>
<Hr style={{ margin: '32px 0' }} />
<Text style={{ color: '#666', fontSize: '12px' }}>
If you were not expecting this invitation, you can safely ignore
this email.
</Text>
</Container>
</Body>
</Html>
)
}
Two things to know about email HTML:
- Inline styles are required. Most email clients strip
<style>tags. Style attributes on each element are the safe path. - Use system fonts. Custom web fonts work in some clients and fail silently in others.
system-ui, sans-serifrenders consistently everywhere.
Step 2: preview the template
Boot the React Email dev server:
npx email dev
This opens http://localhost:3001 with every template in emails/ rendered using mocked props. Iterate on the design without sending real email. Add multiple Props permutations at the bottom of your template file (e.g., long names, missing fields, edge cases) so the preview covers the failure modes too.
Step 3: add a typed send helper
Add a function to lib/resend/send.ts:
// lib/resend/send.ts
import TeamInvite from '@/emails/team-invite'
import { resend } from './client'
import config from '@/config'
interface SendTeamInviteParams {
to: string
inviterName: string
teamName: string
acceptUrl: string
}
export async function sendTeamInviteEmail(params: SendTeamInviteParams) {
const { data, error } = await resend.emails.send({
from: config.email.from,
to: params.to,
subject: `${params.inviterName} invited you to ${params.teamName}`,
react: TeamInvite({
inviterName: params.inviterName,
teamName: params.teamName,
acceptUrl: params.acceptUrl,
}),
})
if (error) {
console.error('Failed to send team invite email:', error)
return { error: 'Failed to send invite email' }
}
return { data }
}
The helper is typed against the template's props, so a missing field fails at the type level rather than at the SMTP layer. Return a generic error to the caller; log the underlying Resend error server-side.
Step 4: trigger from a Server Action or webhook
// actions/team.ts
'use server'
import { sendTeamInviteEmail } from '@/lib/resend/send'
export async function inviteTeamMember(/* ... */) {
// ... validate input, get user, insert invite row ...
const inviteUrl = `${process.env.NEXT_PUBLIC_APP_URL}/team/accept?token=${token}`
await sendTeamInviteEmail({
to: parsed.data.email,
inviterName: user.user_metadata?.full_name || user.email!,
teamName: team.name,
acceptUrl: inviteUrl,
})
return { success: true }
}
For parallel sends (e.g., delivery email to the buyer + notification to the admin), use Promise.all so a slow recipient does not delay the other:
await Promise.all([
sendPurchaseDeliveryEmail(buyerEmail, name, productId),
sendPurchaseNotificationEmail(buyerEmail, name, productId, amount),
])
The Stripe webhook handler uses this pattern for the purchase delivery + notification pair.
Domain verification
For real email to reach inboxes (not the Resend onboarding sender), verify your domain in the Resend dashboard. Resend will ask you to add DNS records (TXT for SPF, DKIM, DMARC). The domain stays in "Pending verification" until DNS propagates; usually 5-30 minutes. Until verification, your from address defaults to Resend's shared sender and your deliverability suffers.
For the deeper deliverability fundamentals (SPF, DKIM, DMARC, sender reputation), see How to send emails in Next.js with React Email and Resend (2026).
Common mistakes
- Sending email from a Client Component. The Resend API key is server-only. Sends must run in a Server Action, an API route, or a webhook handler. A
'use client'file that importsresendwill leak the key into the browser bundle. - Not handling the error case. Resend can return errors (invalid
to, suppression list, rate limit). Always check the error in the response and return a generic error to the caller. - Hardcoding the
fromaddress in the template. Useconfig.email.fromso changing your sending address only touchesconfig.ts, not every template. - Email without a plain-text fallback. React Email auto-generates a plain-text version, but only if the rendered HTML is parseable. Test that the plain-text version is sensible by viewing the source in Gmail or your email client.
What to read next
- Emails feature docs for the broader email surface and the included templates.
- Add a Server Action for the canonical pattern that triggers most emails.