Resend vs Nodemailer vs Postmark: Email Delivery for Node.js in 2026
TL;DR
Resend is the modern choice for 2026 — excellent TypeScript SDK, native React Email integration, and developer-first API that abstracts deliverability. Nodemailer is the universal SMTP client — use it when you control the SMTP server (AWS SES, Gmail, Mailgun) or need custom transport. Postmark is the specialist for transactional deliverability — message streams, template management, and the best inbox placement rates.
Key Takeaways
- Resend: ~500K weekly downloads — modern API, React Email native, generous free tier (3K/month)
- Nodemailer: ~5.2M weekly downloads — SMTP client, works with any provider, no-lock-in
- Postmark: ~95K weekly downloads — highest deliverability focus, message streams, template engine
- Resend is the fastest path to sending email in a Next.js or React app
- Nodemailer is for self-managed SMTP or when you're already paying for AWS SES/Mailgun
- Postmark wins on deliverability for high-volume transactional senders
- React Email (
@react-email/components) works with all three — render to HTML first
Download Trends
| Package | Weekly Downloads | Approach | SMTP? | API? |
|---|---|---|---|---|
nodemailer | ~5.2M | SMTP client | ✅ Any | 🔌 Via providers |
resend | ~500K | HTTP API | ❌ | ✅ Native |
postmark | ~95K | HTTP API | ❌ | ✅ Native |
Resend
Resend launched in 2023 and became the default email choice for the Next.js/Vercel ecosystem quickly. It's designed for developers who want to send email without thinking about deliverability infrastructure.
import { Resend } from "resend"
const resend = new Resend(process.env.RESEND_API_KEY!)
// Simple email:
const { data, error } = await resend.emails.send({
from: "PkgPulse <noreply@pkgpulse.com>",
to: ["user@example.com"],
subject: "Your weekly package report",
html: "<p>Here are your top packages this week...</p>",
})
if (error) {
console.error("Failed to send:", error)
return
}
console.log("Sent:", data?.id)
Resend with React Email (the killer combo):
// emails/weekly-report.tsx — React component as email template:
import {
Html,
Head,
Body,
Container,
Section,
Text,
Heading,
Link,
Hr,
} from "@react-email/components"
interface WeeklyReportProps {
username: string
packages: Array<{ name: string; downloads: number; trend: "up" | "down" }>
}
export function WeeklyReportEmail({ username, packages }: WeeklyReportProps) {
return (
<Html>
<Head />
<Body style={{ fontFamily: "system-ui, sans-serif", background: "#f9fafb" }}>
<Container style={{ maxWidth: "600px", margin: "0 auto", padding: "24px" }}>
<Heading style={{ fontSize: "24px", color: "#111827" }}>
Your Weekly Package Report
</Heading>
<Text style={{ color: "#6b7280" }}>
Hey {username}! Here are your tracked packages this week:
</Text>
{packages.map((pkg) => (
<Section
key={pkg.name}
style={{
padding: "16px",
background: "#fff",
border: "1px solid #e5e7eb",
borderRadius: "8px",
marginBottom: "12px",
}}
>
<Text style={{ margin: 0, fontWeight: "bold" }}>
<Link href={`https://pkgpulse.com/package/${pkg.name}`}>
{pkg.name}
</Link>{" "}
{pkg.trend === "up" ? "📈" : "📉"}
</Text>
<Text style={{ margin: "4px 0 0 0", color: "#6b7280", fontSize: "14px" }}>
{pkg.downloads.toLocaleString()} weekly downloads
</Text>
</Section>
))}
<Hr style={{ borderColor: "#e5e7eb" }} />
<Text style={{ fontSize: "12px", color: "#9ca3af" }}>
<Link href="https://pkgpulse.com/unsubscribe">Unsubscribe</Link>
</Text>
</Container>
</Body>
</Html>
)
}
// Send the React email via Resend:
import { render } from "@react-email/render"
import { WeeklyReportEmail } from "@/emails/weekly-report"
import { Resend } from "resend"
const resend = new Resend(process.env.RESEND_API_KEY!)
// Option 1: Pass component directly (Resend supports this natively):
await resend.emails.send({
from: "PkgPulse <noreply@pkgpulse.com>",
to: [user.email],
subject: "Your Weekly Package Report",
react: WeeklyReportEmail({ username: user.name, packages: userPackages }),
})
// Option 2: Render to HTML manually:
const html = await render(WeeklyReportEmail({ username: user.name, packages: userPackages }))
await resend.emails.send({
from: "PkgPulse <noreply@pkgpulse.com>",
to: [user.email],
subject: "Your Weekly Package Report",
html,
})
Resend in Next.js App Router:
// app/api/send-welcome/route.ts
import { Resend } from "resend"
import { NextRequest, NextResponse } from "next/server"
import { WelcomeEmail } from "@/emails/welcome"
const resend = new Resend(process.env.RESEND_API_KEY!)
export async function POST(req: NextRequest) {
const { email, name } = await req.json()
const { data, error } = await resend.emails.send({
from: "PkgPulse <welcome@pkgpulse.com>",
to: [email],
subject: "Welcome to PkgPulse!",
react: WelcomeEmail({ name }),
})
if (error) {
return NextResponse.json({ error }, { status: 400 })
}
return NextResponse.json({ id: data?.id })
}
Batch sending with Resend:
// Send to multiple recipients in one API call:
const { data, error } = await resend.batch.send([
{
from: "PkgPulse <noreply@pkgpulse.com>",
to: "alice@example.com",
subject: "Weekly report",
react: WeeklyReportEmail({ username: "Alice", packages: alicePackages }),
},
{
from: "PkgPulse <noreply@pkgpulse.com>",
to: "bob@example.com",
subject: "Weekly report",
react: WeeklyReportEmail({ username: "Bob", packages: bobPackages }),
},
])
Nodemailer
Nodemailer is the universal Node.js SMTP client — it connects to any SMTP server and handles transport, attachments, and email formatting.
import nodemailer from "nodemailer"
// Create a transporter (SMTP):
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST, // "smtp.gmail.com", "email-smtp.us-east-1.amazonaws.com", etc.
port: 587, // 587 (STARTTLS) or 465 (SSL)
secure: false, // true for port 465
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
})
// Send an email:
const info = await transporter.sendMail({
from: '"PkgPulse" <noreply@pkgpulse.com>',
to: "user@example.com",
subject: "Your weekly package report",
text: "Plain text fallback...",
html: "<p>Weekly report HTML...</p>",
})
console.log("Message sent:", info.messageId)
Nodemailer with AWS SES (cost-effective at scale):
import nodemailer from "nodemailer"
import aws from "aws-sdk"
// SES transport via nodemailer-ses-transport or @aws-sdk/client-ses:
const transporter = nodemailer.createTransport({
SES: new aws.SES({
region: "us-east-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
}),
sendingRate: 14, // Max 14 messages/second on SES (default quota)
})
await transporter.sendMail({
from: "noreply@pkgpulse.com",
to: "user@example.com",
subject: "Report",
html: "<p>...</p>",
})
Nodemailer with React Email:
import { render } from "@react-email/render"
import { WeeklyReportEmail } from "@/emails/weekly-report"
import nodemailer from "nodemailer"
const transporter = nodemailer.createTransport({ /* your SMTP config */ })
const html = await render(WeeklyReportEmail({ username: "Alice", packages: packages }))
const text = await render(WeeklyReportEmail({ username: "Alice", packages: packages }), { plainText: true })
await transporter.sendMail({
from: "noreply@pkgpulse.com",
to: "alice@example.com",
subject: "Weekly Report",
html,
text, // Plain text fallback for email clients that don't render HTML
})
Nodemailer attachments:
await transporter.sendMail({
from: "noreply@pkgpulse.com",
to: "user@example.com",
subject: "Your package report PDF",
html: "<p>See attached report.</p>",
attachments: [
{
filename: "package-report.pdf",
content: pdfBuffer, // Buffer or Stream
contentType: "application/pdf",
},
{
filename: "data.csv",
path: "/tmp/export.csv", // File path
},
{
filename: "logo.png",
path: "https://pkgpulse.com/logo.png", // URL (Nodemailer downloads it)
cid: "logo@pkgpulse", // CID for inline embedding in HTML
},
],
})
Testing with Nodemailer Ethereal (fake SMTP):
// Create test account (no real emails sent):
const testAccount = await nodemailer.createTestAccount()
const transporter = nodemailer.createTransport({
host: "smtp.ethereal.email",
port: 587,
auth: {
user: testAccount.user,
pass: testAccount.pass,
},
})
const info = await transporter.sendMail({ /* ... */ })
// View in browser:
console.log("Preview:", nodemailer.getTestMessageUrl(info))
Postmark
Postmark is purpose-built for transactional email — it prioritizes inbox placement above everything else.
import postmark from "postmark"
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN!)
// Simple email via API:
await client.sendEmail({
From: "noreply@pkgpulse.com",
To: "user@example.com",
Subject: "Your weekly report",
HtmlBody: "<p>...</p>",
TextBody: "Plain text fallback",
MessageStream: "outbound", // "outbound" or "broadcasts"
})
// Template-based email (stored in Postmark dashboard):
await client.sendEmailWithTemplate({
From: "noreply@pkgpulse.com",
To: "user@example.com",
TemplateAlias: "weekly-report",
TemplateModel: {
username: "Alice",
packages: userPackages,
unsubscribe_url: `https://pkgpulse.com/unsubscribe?token=${token}`,
},
})
Postmark's message streams:
// Transactional stream (default — best deliverability):
await client.sendEmail({
From: "noreply@pkgpulse.com",
To: user.email,
Subject: "Password reset",
HtmlBody: resetEmailHtml,
MessageStream: "outbound", // Transactional
})
// Broadcast stream (for newsletters — separate IP pool):
await client.sendEmail({
From: "newsletter@pkgpulse.com",
To: subscriber.email,
Subject: "PkgPulse Monthly Digest",
HtmlBody: newsletterHtml,
MessageStream: "newsletter", // Broadcast
})
Message streams keep transactional email (password resets, receipts) completely separated from marketing email — a single spam complaint on your newsletter won't tank your transactional deliverability.
Postmark webhooks for tracking:
// app/api/postmark-webhook/route.ts
export async function POST(req: NextRequest) {
const event = await req.json()
switch (event.Type) {
case "Delivery":
await db.emailLogs.update({ messageId: event.MessageID, status: "delivered" })
break
case "Bounce":
await handleBounce(event.Email, event.Type, event.Description)
break
case "SpamComplaint":
await unsubscribeEmail(event.Email)
break
case "Open":
await db.emailLogs.update({ messageId: event.MessageID, openedAt: new Date() })
break
case "Click":
await trackLinkClick(event.MessageID, event.OriginalLink)
break
}
return NextResponse.json({ ok: true })
}
Feature Comparison
| Feature | Resend | Nodemailer | Postmark |
|---|---|---|---|
| React Email native | ✅ | ✅ (render first) | ✅ (render first) |
| TypeScript | ✅ Excellent | ✅ | ✅ |
| SMTP support | ❌ | ✅ Any SMTP | ❌ |
| Templates | ✅ React | HTML/text | ✅ Dashboard templates |
| Webhooks | ✅ | N/A (self-manage) | ✅ Comprehensive |
| Message streams | ❌ | N/A | ✅ (key differentiator) |
| Attachments | ✅ | ✅ Excellent | ✅ |
| Batch sending | ✅ | ✅ | ✅ |
| Free tier | 3K/month | N/A (pay SMTP) | 100/month |
| Pricing | $20/mo (50K) | Pay SMTP provider | $15/mo (10K) |
| Deliverability focus | ✅ Good | ⚠️ Depends on SMTP | ✅ Best-in-class |
| Open/click tracking | ✅ | ❌ | ✅ |
| Suppressions | ✅ | Manual | ✅ |
When to Use Each
Choose Resend if:
- Building a Next.js / React app and want the cleanest DX
- Using React Email for templates (native integration)
- Starting fresh — best free tier, fastest setup
- You want a modern API over SMTP complexity
Choose Nodemailer if:
- You're already paying for AWS SES, Mailgun, or another SMTP provider
- You need attachment handling (complex, multi-part emails)
- You want zero vendor lock-in for email transport
- You need to test locally without sending real emails (Ethereal SMTP)
Choose Postmark if:
- Deliverability is mission-critical (transactional email, receipts)
- You need separate IP pools for transactional vs marketing
- Template management in a dashboard appeals to non-developer team members
- High-volume sending with detailed delivery analytics
Methodology
Download data from npm registry (weekly average, February 2026). Pricing reflects public plans as of March 2026. Feature comparison based on Resend SDK v4.x, Nodemailer v6.x, and Postmark SDK v4.x documentation.
Compare email and notification package ecosystems on PkgPulse →