How to Add Email Sending to Your Node.js App
·PkgPulse Team
TL;DR
Resend for new projects; Nodemailer for existing SMTP infrastructure. Resend (~400K weekly downloads) is the modern email API — excellent DX, native React Email support, 100 emails/day free. Nodemailer (~16M downloads) is the battle-tested SMTP library — works with any email provider. For transactional emails at scale (>100K/month), evaluate SendGrid, Postmark, or AWS SES. Use React Email for template rendering regardless of transport.
Key Takeaways
- Resend: ~400K downloads — modern API, React Email native, free tier
- Nodemailer: ~16M downloads — SMTP/IMAP universal, works with any provider
- React Email — component-based templates that render to HTML (use with either)
- Free tiers: Resend 100/day, Mailgun 100/day, SendGrid 100/day
- Always test email rendering — Outlook breaks most CSS; use Litmus or Email on Acid
Option 1: Resend (Modern API)
npm install resend
// lib/email.ts — Resend setup
import { Resend } from 'resend';
export const resend = new Resend(process.env.RESEND_API_KEY!);
// Send a simple email
export async function sendEmail({
to,
subject,
html,
}: {
to: string;
subject: string;
html: string;
}) {
const { data, error } = await resend.emails.send({
from: 'noreply@yourdomain.com', // Must be verified domain
to,
subject,
html,
});
if (error) throw new Error(error.message);
return data;
}
// With React Email templates (Resend native support)
import { Resend } from 'resend';
import { WelcomeEmail } from './emails/welcome';
const resend = new Resend(process.env.RESEND_API_KEY!);
// Pass React component directly — Resend renders it
await resend.emails.send({
from: 'noreply@yourdomain.com',
to: user.email,
subject: 'Welcome to our app!',
react: <WelcomeEmail userName={user.name} verificationUrl={url} />,
});
// Resend batch sending
const emailList = users.map((user) => ({
from: 'noreply@yourdomain.com',
to: user.email,
subject: 'Monthly Newsletter',
react: <NewsletterEmail user={user} />,
}));
const { data, error } = await resend.batch.send(emailList);
Option 2: Nodemailer (Universal SMTP)
npm install nodemailer
npm install -D @types/nodemailer
// lib/mailer.ts — Nodemailer setup
import nodemailer from 'nodemailer';
// Different transports for different providers:
// Gmail (use app password, not your Google password)
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.GMAIL_USER,
pass: process.env.GMAIL_APP_PASSWORD, // 16-char app password
},
});
// SMTP (generic — works with any SMTP server)
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, // 'smtp.postmarkapp.com'
port: parseInt(process.env.SMTP_PORT!), // 587 (TLS) or 465 (SSL)
secure: process.env.SMTP_PORT === '465',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
// AWS SES via SMTP
const transporter = nodemailer.createTransport({
host: 'email-smtp.us-east-1.amazonaws.com',
port: 587,
auth: {
user: process.env.AWS_SES_SMTP_USERNAME,
pass: process.env.AWS_SES_SMTP_PASSWORD,
},
});
// Verify connection
await transporter.verify();
console.log('SMTP connection verified');
// Sending with Nodemailer
import { render } from '@react-email/render';
import { WelcomeEmail } from './emails/welcome';
async function sendWelcomeEmail(user: { name: string; email: string; verificationUrl: string }) {
// Render React Email template to HTML
const html = await render(
<WelcomeEmail
userName={user.name}
verificationUrl={user.verificationUrl}
userEmail={user.email}
/>
);
const info = await transporter.sendMail({
from: '"My App" <noreply@myapp.com>',
to: user.email,
subject: `Welcome to My App, ${user.name}!`,
html,
// Plain text fallback (good practice)
text: `Welcome ${user.name}! Verify your email: ${user.verificationUrl}`,
});
console.log('Message sent:', info.messageId);
return info;
}
Development: Preview Without Sending
# Option 1: Ethereal (nodemailer test account)
const testAccount = await nodemailer.createTestAccount();
const transporter = nodemailer.createTransport({
host: 'smtp.ethereal.email',
port: 587,
auth: {
user: testAccount.user,
pass: testAccount.pass,
},
});
// After sending: console.log(nodemailer.getTestMessageUrl(info))
# Opens preview URL: https://ethereal.email/message/xyz
# Option 2: React Email dev server (see all templates live)
npx react-email dev
# Opens: localhost:3000 — preview all templates in /emails
Production Best Practices
// 1. Email queue for reliability
// Don't send synchronously in API routes — use a queue
// In API route:
await emailQueue.add('welcome', { userId: user.id });
// Worker:
emailQueue.process('welcome', async (job) => {
const user = await db.user.findUnique({ where: { id: job.data.userId } });
await sendWelcomeEmail(user);
});
// 2. Retry on failure
const { data, error } = await resend.emails.send({ ... });
if (error) {
// Log to error tracker
// Add to retry queue
// Don't throw — don't fail the user's request over email
}
// 3. SPF/DKIM/DMARC
// Required for deliverability — set up in your DNS:
// SPF: "v=spf1 include:amazonses.com ~all"
// DKIM: configure via your email provider
// DMARC: "v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com"
// 4. Unsubscribe link
// Required by law (CAN-SPAM, GDPR) for marketing emails
// Transactional emails (receipts, verification) don't require it
Provider Comparison
| Provider | Free Tier | Best For |
|---|---|---|
| Resend | 100/day, 3K/month | New projects, DX-first |
| SendGrid | 100/day | Enterprise, detailed analytics |
| Postmark | 100/month | Transactional, high deliverability |
| Mailgun | 100/day | Developers, API-first |
| AWS SES | $0.10/1K | High volume, AWS stack |
| Nodemailer | N/A (needs SMTP) | Self-hosted, any provider |
Compare email library package health on PkgPulse.
See the live comparison
View resend vs. sendgrid on PkgPulse →