Email is deceptively hard. Sending an email is easy, getting it to land in the inbox instead of spam is the hard part. Email deliverability is a world of SPF records, DKIM signatures, reputation scores, and blocklists. That's why you use an email service providerWhat is provider?A wrapper component that makes data available to all components nested inside it without passing props manually. rather than sending from your own server.
Transactional vs marketing email
Not all email is the same. The infrastructure and rules differ:
| Type | Examples | Characteristics |
|---|---|---|
| Transactional | Welcome, password reset, receipt, alert | Triggered by user action, high urgency, must arrive |
| Marketing | Newsletters, promotions, announcements | Sent in bulk, recipient can unsubscribe |
This lesson focuses on transactional email. Marketing email has additional requirements (CAN-SPAM compliance, unsubscribe links) that are usually handled by tools like Mailchimp or ConvertKit.
Sending email with Resend
Resend is the simplest modern email APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.. Clean interface, good TypeScript support, and it pairs naturally with React Email for templating.
npm install resendimport { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
// Send a simple email
async function sendWelcomeEmail(userEmail: string, userName: string) {
const { data, error } = await resend.emails.send({
from: 'HACKUP <onboarding@hackup.dev>', // Must be a verified domain
to: userEmail,
subject: 'Welcome to HACKUP',
html: `<h1>Hey ${userName}!</h1><p>Your account is ready. Start learning.</p>`
});
if (error) {
console.error('Email failed:', error);
return { success: false };
}
return { success: true, id: data?.id };
}React Email templates
Hardcoded HTML strings in your JavaScript are painful to maintain. React Email lets you write email templates as React components, which you then render to HTML before sending.
npm install @react-email/components react-dom// emails/WelcomeEmail.tsx
import {
Html,
Head,
Body,
Container,
Heading,
Text,
Button,
Hr
} from '@react-email/components';
interface WelcomeEmailProps {
userName: string;
dashboardUrl: string;
}
export function WelcomeEmail({ userName, dashboardUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'sans-serif', backgroundColor: '#f4f4f4' }}>
<Container style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
<Heading>Welcome, {userName}!</Heading>
<Text>Your account is set up and ready to go.</Text>
<Button
href={dashboardUrl}
style={{ backgroundColor: '#22c55e', color: 'white', padding: '12px 24px' }}
>
Go to dashboard
</Button>
<Hr />
<Text style={{ fontSize: '12px', color: '#666' }}>
You received this because you signed up at hackup.dev.
</Text>
</Container>
</Body>
</Html>
);
}// Render the component to HTML and send it
import { render } from '@react-email/render';
import { WelcomeEmail } from './emails/WelcomeEmail';
async function sendWelcomeEmail(user: { email: string; name: string }) {
const html = render(
<WelcomeEmail
userName={user.name}
dashboardUrl={`${process.env.APP_URL}/dashboard`}
/>
);
await resend.emails.send({
from: 'HACKUP <onboarding@hackup.dev>',
to: user.email,
subject: 'Welcome to HACKUP',
html
});
}DNSWhat is dns?The system that translates human-readable domain names like google.com into the numerical IP addresses computers use to find each other. configuration for deliverability
Before you can send from your own domain, you need to add DNS records that prove you own it and authorize Resend (or SendGrid) to send on your behalf. Without these, your email will land in spam or be rejected entirely.
# SPF - lists which servers are allowed to send email for your domain
Type: TXT
Name: @
Value: v=spf1 include:_spf.resend.com ~all
# DKIM - a cryptographic signature Resend adds to each email
# Resend gives you the exact record to add in their dashboard
Type: CNAME
Name: resend._domainkey
Value: [provided by Resend]
# DMARC - tells receiving servers what to do with emails that fail SPF/DKIM
Type: TXT
Name: _dmarc
Value: v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.comSendGrid as an alternative
SendGrid is older and more feature-rich than Resend, but its APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. is more verbose. It's a good choice if you need advanced features like dynamic templates, suppression groups, or very high volume.
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
async function sendPasswordReset(email: string, resetToken: string) {
const resetUrl = `${process.env.APP_URL}/reset-password?token=${resetToken}`;
await sgMail.send({
to: email,
from: 'support@yourdomain.com', // Must be a verified sender
subject: 'Reset your password',
templateId: 'd-abc123', // Optional: SendGrid dynamic template
dynamicTemplateData: {
reset_url: resetUrl,
expiry: '1 hour'
}
});
}Quick reference
| Feature | Resend | SendGrid |
|---|---|---|
| Free tier | 3,000 emails/month | 100 emails/day |
| React Email support | Native | Via render |
| TypeScript | Excellent | Good |
| Dynamic templates | Basic | Advanced |
| Best for | Modern apps, prototypes | High volume, enterprise |
// Complete email service
class EmailService {
constructor() {
this.resend = new Resend(process.env.RESEND_API_KEY);
}
async sendWelcomeEmail(user) {
try {
await this.resend.emails.send({
from: 'Acme <onboarding@acme.com>',
to: user.email,
subject: 'Welcome to Acme!',
html: `
<h1>Hello ${user.name}!</h1>
<p>Thank you for signing up.</p>
<a href="${process.env.APP_URL}/dashboard">
Go to my account
</a>
`
});
} catch (error) {
console.error('Email failed:', error);
// Don't block user flow if email fails
}
}
async sendPasswordReset(user, token) {
const resetUrl = `${process.env.APP_URL}/reset-password?token=${token}`;
await this.resend.emails.send({
from: 'Acme <support@acme.com>',
to: user.email,
subject: 'Password reset',
html: `
<p>Click this link to reset your password:</p>
<a href="${resetUrl}">${resetUrl}</a>
<p>This link expires in 1 hour.</p>
`
});
}
}