Node.js Transactional Email: Complete Integration Guide
Everything you need to send reliable transactional emails from Node.js applications
Transactional emails are the backbone of modern web applications. According to Mailgun research, transactional emails have an average open rate of 80-85% compared to just 20-25% for marketing emails. They include password resets, order confirmations, shipping notifications, and account alerts.
This comprehensive guide shows you how to implement production-ready transactional email in Node.js using NotiGrid and popular email providers like AWS SES, Resend, and SendGrid.
What you will learn:
- Choose the right email provider for your needs
- Install and configure the NotiGrid SDK
- Set up email integrations with multiple providers
- Create dynamic email templates with variables
- Send transactional emails with proper error handling
- Implement best practices for deliverability
- Monitor and debug email delivery
What Are Transactional Emails?
Transactional emails are triggered by user actions or system events, not marketing campaigns. According to Postmark's email statistics, they are expected by users and have significantly higher engagement rates.
Common Transactional Email Types
| Category | Examples | Priority |
|---|---|---|
| Authentication | Password reset, email verification, 2FA codes | Critical |
| Commerce | Order confirmation, shipping updates, receipts | High |
| Account | Welcome email, profile changes, subscription updates | Medium |
| Alerts | Security warnings, payment failures, usage limits | Critical |
| Notifications | Comments, mentions, activity updates | Medium |
Why Transactional Email Matters
- 80%+ open rates vs 20% for marketing emails (Campaign Monitor)
- Password resets must arrive within seconds or users abandon the flow
- Order confirmations reduce support tickets by 30%+ (Zendesk)
- Security alerts are legally required in many jurisdictions
Choosing an Email Provider
Before writing code, select an email service provider. Here is how the major providers compare:
Provider Comparison
| Provider | Free Tier | Price per 1K | Best For |
|---|---|---|---|
| AWS SES | 62K/month (from EC2) | 0.10 USD | High volume, AWS users |
| Resend | 3K/month | 1.00 USD | Developer experience |
| SendGrid | 100/day | 0.50 USD | Marketing + transactional |
| Postmark | 100/month | 1.25 USD | Deliverability focused |
| Mailgun | 5K/month (3 months) | 0.80 USD | Flexible APIs |
When to Use Each Provider
AWS SES - Best for:
- High-volume senders (millions of emails)
- Teams already using AWS infrastructure
- Cost-sensitive applications
- Documentation
Resend - Best for:
- Modern developer experience
- React email templates
- Quick setup and iteration
- Documentation
SendGrid - Best for:
- Combined marketing and transactional
- Advanced analytics
- Enterprise compliance needs
- Documentation
Postmark - Best for:
- Maximum deliverability
- Strict transactional-only sending
- Detailed delivery metrics
- Documentation
Prerequisites
Before beginning, ensure you have:
- A NotiGrid account (sign up free)
- Node.js 18+ installed (download)
- An email provider account (AWS SES, Resend, or SendGrid)
- A verified sending domain
- Basic knowledge of TypeScript/JavaScript
Step 1: Install the NotiGrid SDK
Install the SDK using your preferred package manager:
# npm
npm install @notigrid/sdk
# yarn
yarn add @notigrid/sdk
# pnpm
pnpm add @notigrid/sdkThe NotiGrid SDK is fully typed and supports both CommonJS and ES modules.
Step 2: Initialize the Client
Create a NotiGrid client instance with your API key:
import { NotiGrid } from '@notigrid/sdk'
// Initialize with API key from environment
const notigrid = new NotiGrid({
apiKey: process.env.NOTIGRID_API_KEY,
// Optional: Enable debug logging
debug: process.env.NODE_ENV === 'development'
})
// Verify connection (optional but recommended)
async function verifyConnection() {
try {
const status = await notigrid.health.check()
console.log('NotiGrid connected:', status.healthy)
} catch (error) {
console.error('Failed to connect to NotiGrid:', error)
process.exit(1)
}
}Environment Variables
Create a .env file in your project root:
# NotiGrid
NOTIGRID_API_KEY=ng_live_xxxxxxxxxxxx
# AWS SES (if using)
AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_SES_REGION=us-east-1
# Resend (if using)
RESEND_API_KEY=re_xxxxxxxxxxxx
# SendGrid (if using)
SENDGRID_API_KEY=SG.xxxxxxxxxxxxNever commit API keys to version control. Use dotenv or your platform's secret management.
Step 3: Configure Email Integration
Set up your email provider in the NotiGrid dashboard or via API.
AWS SES Configuration
AWS SES offers the lowest cost per email but requires domain verification and potentially moving out of sandbox mode.
const sesIntegration = await notigrid.integrations.create({
name: 'aws-ses-production',
type: 'email',
provider: 'ses',
config: {
region: process.env.AWS_SES_REGION,
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
fromEmail: 'notifications@yourdomain.com',
fromName: 'Your App Name'
}
})
console.log('SES Integration ID:', sesIntegration.id)Important SES Setup Steps:
- Verify your domain in SES console
- Request production access to send to any address
- Set up DKIM signing for better deliverability
- Configure SPF records
Resend Configuration
Resend provides a modern API with excellent developer experience:
const resendIntegration = await notigrid.integrations.create({
name: 'resend-production',
type: 'email',
provider: 'resend',
config: {
apiKey: process.env.RESEND_API_KEY,
fromEmail: 'hello@yourdomain.com',
fromName: 'Your App'
}
})SendGrid Configuration
SendGrid is ideal for teams needing both transactional and marketing capabilities:
const sendgridIntegration = await notigrid.integrations.create({
name: 'sendgrid-production',
type: 'email',
provider: 'sendgrid',
config: {
apiKey: process.env.SENDGRID_API_KEY,
fromEmail: 'noreply@yourdomain.com',
fromName: 'Your Company'
}
})Step 4: Create Email Templates
Templates separate your email content from your application code. This enables non-developers to update copy without deployments.
Basic Template
const welcomeTemplate = await notigrid.templates.create({
name: 'welcome-email',
type: 'email',
subject: 'Welcome to [appName], [firstName]!',
body: `
<h1>Welcome aboard, [firstName]!</h1>
<p>Thanks for joining [appName]. We are excited to have you.</p>
<p>Here is what you can do next:</p>
<ul>
<li>Complete your profile</li>
<li>Explore our features</li>
<li>Invite your team</li>
</ul>
<a href="[dashboardUrl]" style="
display: inline-block;
background: #2563eb;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
">Go to Dashboard</a>
<p>Questions? Reply to this email or visit our <a href="[helpUrl]">help center</a>.</p>
<p>Best,<br>The [appName] Team</p>
`,
variables: ['firstName', 'appName', 'dashboardUrl', 'helpUrl']
})Order Confirmation Template
const orderTemplate = await notigrid.templates.create({
name: 'order-confirmation',
type: 'email',
subject: 'Order #[orderId] Confirmed',
body: `
<h1>Thanks for your order, [customerName]!</h1>
<p>We have received your order and it is being processed.</p>
<div style="background: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h2 style="margin-top: 0;">Order Details</h2>
<p><strong>Order Number:</strong> [orderId]</p>
<p><strong>Order Date:</strong> [orderDate]</p>
<p><strong>Total:</strong> [orderTotal]</p>
</div>
<h3>Shipping Address</h3>
<p>[shippingAddress]</p>
<p><strong>Estimated Delivery:</strong> [deliveryDate]</p>
<a href="[trackingUrl]" style="
display: inline-block;
background: #2563eb;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
">Track Your Order</a>
`,
variables: [
'customerName',
'orderId',
'orderDate',
'orderTotal',
'shippingAddress',
'deliveryDate',
'trackingUrl'
]
})Password Reset Template
Password reset emails are security-critical. Keep them simple and clear:
const passwordResetTemplate = await notigrid.templates.create({
name: 'password-reset',
type: 'email',
subject: 'Reset your [appName] password',
body: `
<h1>Password Reset Request</h1>
<p>Hi [firstName],</p>
<p>We received a request to reset your password. Click the button below to create a new password:</p>
<a href="[resetUrl]" style="
display: inline-block;
background: #dc2626;
color: white;
padding: 14px 28px;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
">Reset Password</a>
<p style="margin-top: 20px; color: #6b7280; font-size: 14px;">
This link expires in [expiryTime]. If you did not request this reset,
please ignore this email or contact support if you have concerns.
</p>
<p style="color: #6b7280; font-size: 12px;">
For security, this request was received from [ipAddress] using [userAgent].
</p>
`,
variables: ['firstName', 'appName', 'resetUrl', 'expiryTime', 'ipAddress', 'userAgent']
})Step 5: Create Email Channels
Channels define how notifications are delivered, including fallback providers:
// Single provider channel
const orderChannel = await notigrid.channels.create({
name: 'order-notifications',
description: 'Order confirmation and shipping updates',
steps: [
{
order: 0,
type: 'email',
integrationId: sesIntegration.id,
templateId: orderTemplate.id,
retries: 3
}
]
})
// Multi-provider channel with fallback
const criticalChannel = await notigrid.channels.create({
name: 'critical-emails',
description: 'Password resets and security alerts with fallback',
steps: [
{
order: 0,
type: 'email',
integrationId: sesIntegration.id,
templateId: passwordResetTemplate.id,
retries: 2
},
{
order: 1,
type: 'email',
integrationId: resendIntegration.id, // Fallback to Resend
templateId: passwordResetTemplate.id,
retries: 2,
delay: 30 // Wait 30 seconds before fallback
}
]
})Step 6: Send Transactional Emails
Now you can send emails with full variable substitution:
Basic Send
await notigrid.notify({
channelId: 'order-notifications',
to: 'customer@example.com',
variables: {
customerName: 'Jane Smith',
orderId: 'ORD-2024-12345',
orderDate: 'December 12, 2025',
orderTotal: '149.99 USD',
shippingAddress: '123 Main St, San Francisco, CA 94102',
deliveryDate: 'December 15-17, 2025',
trackingUrl: 'https://yourapp.com/track/ORD-2024-12345'
}
})Send with Error Handling
async function sendOrderConfirmation(order: Order, customer: Customer) {
try {
const result = await notigrid.notify({
channelId: 'order-notifications',
to: customer.email,
variables: {
customerName: customer.name,
orderId: order.id,
orderDate: formatDate(order.createdAt),
orderTotal: formatCurrency(order.total),
shippingAddress: formatAddress(order.shippingAddress),
deliveryDate: order.estimatedDelivery,
trackingUrl: 'https://yourapp.com/track/' + order.id
},
metadata: {
orderId: order.id,
customerId: customer.id
}
})
console.log('Email sent:', result.messageId)
// Update order with notification status
await db.orders.update(order.id, {
confirmationSentAt: new Date(),
notificationId: result.messageId
})
return result
} catch (error) {
console.error('Failed to send order confirmation:', error)
// Log failure for retry or manual intervention
await db.failedNotifications.create({
type: 'order-confirmation',
orderId: order.id,
error: error.message,
timestamp: new Date()
})
throw error
}
}Batch Sending
For sending multiple emails efficiently:
async function sendBulkNotifications(users: User[], template: string) {
const results = await Promise.allSettled(
users.map(user =>
notigrid.notify({
channelId: 'weekly-digest',
to: user.email,
variables: {
firstName: user.firstName,
digestContent: generateDigest(user)
}
})
)
)
const succeeded = results.filter(r => r.status === 'fulfilled').length
const failed = results.filter(r => r.status === 'rejected').length
console.log('Batch send complete:', succeeded, 'sent,', failed, 'failed')
return { succeeded, failed, results }
}Step 7: Monitor Email Delivery
Check Delivery Status
// Get notification status
const status = await notigrid.notifications.get(messageId)
console.log('Status:', status.status) // queued, sent, delivered, failed
console.log('Provider:', status.provider)
console.log('Sent at:', status.sentAt)
console.log('Delivered at:', status.deliveredAt)Set Up Webhooks for Real-Time Updates
Configure webhooks to receive delivery events:
// In your Express/Fastify app
app.post('/webhooks/notigrid', async (req, res) => {
const event = req.body
switch (event.type) {
case 'notification.delivered':
console.log('Email delivered:', event.data.messageId)
await updateDeliveryStatus(event.data.messageId, 'delivered')
break
case 'notification.bounced':
console.log('Email bounced:', event.data.messageId)
await handleBounce(event.data.recipient, event.data.bounceType)
break
case 'notification.complained':
console.log('Spam complaint:', event.data.messageId)
await handleComplaint(event.data.recipient)
break
case 'notification.failed':
console.log('Delivery failed:', event.data.messageId)
await handleFailure(event.data)
break
}
res.status(200).send('OK')
})Email Deliverability Best Practices
Following Google's sender guidelines and Yahoo's requirements is essential for inbox placement.
1. Authentication Setup
Configure all three authentication methods:
SPF (Sender Policy Framework)
v=spf1 include:amazonses.com include:_spf.google.com ~allDKIM (DomainKeys Identified Mail)
- Enable in your email provider dashboard
- Add the CNAME/TXT records to your DNS
DMARC (Domain-based Message Authentication)
v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com2. Maintain a Clean Sender Reputation
// Handle bounces immediately
async function handleBounce(email: string, bounceType: string) {
if (bounceType === 'permanent') {
// Remove from mailing list
await db.users.update({ email }, {
emailBounced: true,
emailBouncedAt: new Date()
})
}
}
// Honor unsubscribes
async function handleUnsubscribe(email: string) {
await db.users.update({ email }, {
emailOptOut: true,
optOutAt: new Date()
})
}3. Content Best Practices
- Keep subject lines under 50 characters
- Use a clear sender name users recognize
- Include a plain text version
- Avoid spam trigger words (free, urgent, act now)
- Include physical mailing address (required by CAN-SPAM)
4. Technical Optimization
// Add List-Unsubscribe header for easy opt-out
const emailOptions = {
headers: {
'List-Unsubscribe': '<https://yourapp.com/unsubscribe?token=xxx>',
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click'
}
}Advanced Features
Email Attachments
import { readFileSync } from 'fs'
await notigrid.notify({
channelId: 'invoice-emails',
to: customer.email,
variables: {
customerName: customer.name,
invoiceNumber: invoice.id
},
attachments: [
{
filename: 'invoice-' + invoice.id + '.pdf',
content: readFileSync('./invoices/' + invoice.id + '.pdf'),
contentType: 'application/pdf'
}
]
})Conditional Content
Use template logic for dynamic content:
const template = await notigrid.templates.create({
name: 'subscription-update',
type: 'email',
subject: 'Your subscription has been updated',
body: `
<h1>Subscription Update</h1>
[#if isUpgrade]
<p>Congratulations! You have upgraded to [planName].</p>
<p>Your new features include:</p>
<ul>
[#each newFeatures]
<li>[this]</li>
[/each]
</ul>
[else]
<p>Your subscription has been changed to [planName].</p>
[/if]
<p>New billing amount: [billingAmount]/[billingCycle]</p>
`,
variables: ['isUpgrade', 'planName', 'newFeatures', 'billingAmount', 'billingCycle']
})Scheduling Emails
// Send email at a specific time
await notigrid.notify({
channelId: 'reminder-emails',
to: user.email,
variables: {
eventName: event.name,
eventTime: event.startTime
},
scheduledFor: new Date(event.startTime.getTime() - 24 * 60 * 60 * 1000) // 24 hours before
})Multi-Language Support
// Store templates for each language
const templates = {
en: 'welcome-email-en',
es: 'welcome-email-es',
fr: 'welcome-email-fr',
de: 'welcome-email-de'
}
await notigrid.notify({
channelId: 'welcome-' + user.language,
to: user.email,
variables: {
firstName: user.firstName,
appName: getLocalizedAppName(user.language)
}
})Troubleshooting Common Issues
Emails Not Sending
- Check API key permissions - Ensure your key has send permissions
- Verify sender domain - Domain must be verified with your provider
- Check sandbox mode - AWS SES requires production access request
- Review rate limits - You may be hitting provider limits
Emails Going to Spam
- Check authentication - Verify SPF, DKIM, and DMARC are configured
- Review content - Avoid spam trigger words and excessive links
- Check sender reputation - Use Google Postmaster Tools
- Warm up IP - Gradually increase send volume for new IPs
Template Variables Not Replacing
// Wrong - using curly braces
subject: 'Hello {{name}}'
// Correct - using square brackets for NotiGrid
subject: 'Hello [name]'High Bounce Rates
- Implement email verification at signup
- Remove hard bounces immediately
- Use double opt-in for subscriptions
- Regularly clean your email list
Frequently Asked Questions
What is the difference between transactional and marketing email?
Transactional emails are triggered by user actions (password resets, order confirmations, account alerts) and are expected by recipients. Marketing emails are promotional content sent to lists of subscribers. Transactional emails typically have 80%+ open rates vs 20% for marketing, and have different legal requirements under CAN-SPAM and GDPR.
Which email provider should I choose for Node.js?
For most Node.js applications, we recommend AWS SES for high-volume cost efficiency, Resend for developer experience and modern APIs, or SendGrid for combined marketing and transactional needs. NotiGrid supports all major providers and enables easy switching.
How do I improve email deliverability?
The key factors are: 1) Set up SPF, DKIM, and DMARC authentication, 2) Maintain list hygiene by removing bounces immediately, 3) Use a consistent sender name and address, 4) Avoid spam trigger words, 5) Gradually warm up new sending IPs. See Google's sender guidelines for detailed requirements.
Can I use multiple email providers with NotiGrid?
Yes, NotiGrid supports multi-provider configurations. You can set up automatic failover (if SES fails, try Resend), A/B testing between providers, or route different email types to different providers. This ensures maximum deliverability and uptime.
How do I handle email bounces and complaints?
NotiGrid provides webhooks for bounce and complaint notifications. Hard bounces should immediately mark the email as invalid in your database. Spam complaints should trigger automatic unsubscription. Maintaining list hygiene is critical for sender reputation.
What are the rate limits for transactional email?
Rate limits vary by provider: AWS SES allows 14 emails/second by default (increasable), Resend allows 10/second on free tier, SendGrid varies by plan. NotiGrid automatically handles rate limiting and queuing to prevent failures.
Summary
Implementing transactional email in Node.js requires:
- Choosing the right provider - AWS SES for volume, Resend for DX, SendGrid for features
- Proper authentication - SPF, DKIM, and DMARC are essential
- Template management - Separate content from code
- Error handling - Implement retries and fallbacks
- Monitoring - Track delivery rates and handle bounces
NotiGrid simplifies all of this with a unified API, automatic retries, multi-provider failover, and comprehensive logging.
Next Steps
Ready to implement transactional email?
- Getting Started with NotiGrid - Send your first notification in 15 minutes
- Email vs Slack vs SMS Channel Guide - Choose the right channel for each use case
- Building Multi-Channel Notifications - Complete architecture guide
- Template Guide - Advanced template techniques
Need Help?
Email Support: support@notigrid.com Schedule a Demo: notigrid.com/demo Documentation: docs.notigrid.com
Ready to send your first notification?
Get started with NotiGrid today and send notifications across email, SMS, Slack, and more.