Sending Emails
Send transactional emails with Nodemailer, SendGrid, Resend, and AWS SES.
5 min read
Email Options#
| Service | Best For | Pricing |
|---|---|---|
| Nodemailer | Dev, SMTP | Free (+ SMTP costs) |
| Resend | Modern DX | Free tier, then usage |
| SendGrid | Scale, templates | Free tier, then usage |
| AWS SES | AWS, high volume | Very cheap at scale |
| Postmark | Deliverability | Paid, reliable |
Option 1: Resend (Modern)#
Simple, great DX, React email support:
bash
npm install resend
javascript
// src/services/email.js
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendEmail({ to, subject, html, text }) {
const { data, error } = await resend.emails.send({
from: 'MyApp <noreply@myapp.com>',
to,
subject,
html,
text,
});
if (error) {
throw new Error(`Email failed: ${error.message}`);
}
return data;
}
// Usage
await sendEmail({
to: 'user@example.com',
subject: 'Welcome!',
html: '<h1>Welcome to MyApp!</h1>',
});
With React Email Templates#
bash
npm install @react-email/components
jsx
// emails/welcome.jsx
import { Html, Head, Body, Container, Text, Button } from '@react-email/components';
export function WelcomeEmail({ name, loginUrl }) {
return (
<Html>
<Head />
<Body style={{ fontFamily: 'sans-serif' }}>
<Container>
<Text>Hi {name},</Text>
<Text>Welcome to MyApp! Click below to get started:</Text>
<Button href={loginUrl} style={{ background: '#007bff', color: '#fff' }}>
Log In
</Button>
</Container>
</Body>
</Html>
);
}
javascript
// src/services/email.js
import { render } from '@react-email/render';
import { WelcomeEmail } from '../emails/welcome.jsx';
export async function sendWelcomeEmail(user) {
const html = render(WelcomeEmail({
name: user.name,
loginUrl: `${process.env.APP_URL}/login`,
}));
return resend.emails.send({
from: 'MyApp <noreply@myapp.com>',
to: user.email,
subject: 'Welcome to MyApp!',
html,
});
}
Option 2: SendGrid#
bash
npm install @sendgrid/mail
javascript
// src/services/email.js
import sgMail from '@sendgrid/mail';
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
export async function sendEmail({ to, subject, html, text }) {
await sgMail.send({
to,
from: 'noreply@myapp.com',
subject,
html,
text,
});
}
// With template
export async function sendTemplateEmail({ to, templateId, data }) {
await sgMail.send({
to,
from: 'noreply@myapp.com',
templateId, // SendGrid template ID
dynamicTemplateData: data,
});
}
// Usage
await sendTemplateEmail({
to: 'user@example.com',
templateId: 'd-xxxxxxxxxxxxx',
data: {
name: 'John',
orderNumber: '12345',
},
});
Option 3: Nodemailer (SMTP)#
Works with any SMTP provider:
bash
npm install nodemailer
javascript
// src/services/email.js
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
export async function sendEmail({ to, subject, html, text }) {
return transporter.sendMail({
from: '"MyApp" <noreply@myapp.com>',
to,
subject,
html,
text,
});
}
// Verify connection on startup
transporter.verify((error) => {
if (error) {
console.error('SMTP connection failed:', error);
} else {
console.log('SMTP ready');
}
});
For Development (Ethereal)#
javascript
// Create test account
const testAccount = await nodemailer.createTestAccount();
const transporter = nodemailer.createTransport({
host: 'smtp.ethereal.email',
port: 587,
auth: {
user: testAccount.user,
pass: testAccount.pass,
},
});
// Get preview URL after sending
const info = await transporter.sendMail({ /* ... */ });
console.log('Preview URL:', nodemailer.getTestMessageUrl(info));
Option 4: AWS SES#
bash
npm install @aws-sdk/client-ses
javascript
// src/services/email.js
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
const ses = new SESClient({ region: 'us-east-1' });
export async function sendEmail({ to, subject, html, text }) {
const command = new SendEmailCommand({
Source: 'noreply@myapp.com',
Destination: {
ToAddresses: Array.isArray(to) ? to : [to],
},
Message: {
Subject: { Data: subject },
Body: {
Html: { Data: html },
Text: { Data: text },
},
},
});
return ses.send(command);
}
Email Templates#
Simple Template Function#
javascript
// src/emails/templates.js
export function welcomeEmail({ name }) {
return {
subject: 'Welcome to MyApp!',
html: `
<!DOCTYPE html>
<html>
<body style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h1>Welcome, ${name}!</h1>
<p>We're excited to have you on board.</p>
<a href="${process.env.APP_URL}/dashboard"
style="display: inline-block; padding: 12px 24px; background: #007bff; color: white; text-decoration: none; border-radius: 4px;">
Go to Dashboard
</a>
</body>
</html>
`,
text: `Welcome, ${name}! We're excited to have you on board. Visit ${process.env.APP_URL}/dashboard to get started.`,
};
}
export function passwordResetEmail({ name, resetUrl }) {
return {
subject: 'Reset Your Password',
html: `
<h1>Password Reset</h1>
<p>Hi ${name}, click the link below to reset your password:</p>
<a href="${resetUrl}">Reset Password</a>
<p>This link expires in 1 hour.</p>
`,
text: `Hi ${name}, visit this link to reset your password: ${resetUrl}`,
};
}
Usage#
javascript
import { sendEmail } from '../services/email.js';
import { welcomeEmail, passwordResetEmail } from '../emails/templates.js';
// Send welcome email
async function onUserSignup(user) {
const template = welcomeEmail({ name: user.name });
await sendEmail({
to: user.email,
...template,
});
}
// Send password reset
async function onPasswordResetRequest(user, token) {
const resetUrl = `${process.env.APP_URL}/reset-password?token=${token}`;
const template = passwordResetEmail({ name: user.name, resetUrl });
await sendEmail({
to: user.email,
...template,
});
}
Queue Emails (Don't Block API)#
javascript
// src/queues/email.js
import { Queue, Worker } from 'bullmq';
const emailQueue = new Queue('email', {
connection: { host: 'localhost', port: 6379 },
});
// Add to queue instead of sending directly
export async function queueEmail(emailData) {
await emailQueue.add('send', emailData, {
attempts: 3,
backoff: { type: 'exponential', delay: 5000 },
});
}
// Worker processes the queue
const worker = new Worker('email', async (job) => {
await sendEmail(job.data);
}, {
connection: { host: 'localhost', port: 6379 },
});
// In your API
router.post('/signup', async (req, res) => {
const user = await UserService.create(req.body);
// Queue email (non-blocking)
await queueEmail({
to: user.email,
...welcomeEmail({ name: user.name }),
});
res.status(201).json({ data: user });
});
Comparison#
| Feature | Resend | SendGrid | Nodemailer | AWS SES |
|---|---|---|---|---|
| Ease of use | Best | Good | Medium | Medium |
| React templates | Yes | No | Manual | No |
| Price (10k/mo) | Free | Free | SMTP cost | ~$1 |
| Deliverability | Good | Excellent | Varies | Good |
| Analytics | Basic | Excellent | No | Basic |
Key Takeaways#
- Queue emails - Don't block API responses
- Use templates - Consistent, maintainable
- Resend for simplicity - Great DX, React support
- SendGrid for scale - Templates, analytics
- Always include text - For spam filters
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.