Skip to main content

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

ProviderFree TierBest For
Resend100/day, 3K/monthNew projects, DX-first
SendGrid100/dayEnterprise, detailed analytics
Postmark100/monthTransactional, high deliverability
Mailgun100/dayDevelopers, API-first
AWS SES$0.10/1KHigh volume, AWS stack
NodemailerN/A (needs SMTP)Self-hosted, any provider

Compare email library package health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.