townkrier-resend
v1.0.1-alpha.1
Published
Resend email adapter for Townkrier notification system
Maintainers
Readme
townkrier-resend
Resend email adapter for the TownKrier notification system.
Features
- 📧 Modern email delivery via Resend API
- 🎨 HTML and plain text email support
- 📎 File attachments support
- 🔄 Automatic retry with exponential backoff
- 📊 Delivery tracking and webhooks
- 🎯 Template support
- 🌐 Multiple from addresses
- 🔒 Secure authentication
Installation
npm install townkrier-resend townkrier-core
# or
pnpm add townkrier-resend townkrier-coreQuick Start
import { NotificationManager, NotificationChannel } from 'townkrier-core';
import { createResendChannel } from 'townkrier-resend';
// Configure the manager with Resend channel
const manager = new NotificationManager({
defaultChannel: 'email-resend',
channels: [
{
name: 'email-resend',
enabled: true,
config: {
apiKey: process.env.RESEND_API_KEY,
from: '[email protected]',
fromName: 'Your App',
},
},
],
});
// Register the Resend channel factory
manager.registerFactory('email-resend', createResendChannel);
// Create a notification
class WelcomeEmailNotification extends Notification {
constructor(private userName: string) {
super();
}
via() {
return [NotificationChannel.EMAIL];
}
toEmail() {
return {
subject: 'Welcome to Our App! 🎉',
html: `
<h1>Welcome ${this.userName}!</h1>
<p>Thanks for joining our service.</p>
`,
text: `Welcome ${this.userName}! Thanks for joining our service.`,
};
}
}
// Send the notification
const notification = new WelcomeEmailNotification('John');
const recipient = {
[NotificationChannel.EMAIL]: {
email: '[email protected]',
name: 'John Doe',
},
};
await manager.send(notification, recipient);Configuration
ResendConfig
interface ResendConfig {
apiKey: string; // Required: Your Resend API key
from?: string; // Optional: Default from email address
fromName?: string; // Optional: Default from name
timeout?: number; // Optional: Request timeout in ms (default: 30000)
debug?: boolean; // Optional: Enable debug logging (default: false)
}Getting Your API Key
- Sign up at resend.com
- Verify your domain in the dashboard
- Navigate to API Keys section
- Create a new API key
- Store it securely in your environment variables
Domain Verification
Before sending emails, you must verify your domain:
- Go to Resend Dashboard → Domains
- Add your domain
- Add the provided DNS records (SPF, DKIM)
- Wait for verification (usually a few minutes)
Advanced Usage
Rich HTML Emails
class OrderConfirmationNotification extends Notification {
constructor(
private orderNumber: string,
private items: Array<{ name: string; price: number }>,
private total: number,
) {
super();
}
via() {
return [NotificationChannel.EMAIL];
}
toEmail() {
const itemsList = this.items.map((item) => `<li>${item.name} - $${item.price}</li>`).join('');
return {
subject: `Order Confirmation #${this.orderNumber}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #333;">Order Confirmed! ✅</h1>
<p>Thank you for your order. Here's what you ordered:</p>
<ul style="list-style: none; padding: 0;">
${itemsList}
</ul>
<hr />
<p style="font-size: 18px; font-weight: bold;">
Total: $${this.total.toFixed(2)}
</p>
<a href="https://yourapp.com/orders/${this.orderNumber}"
style="display: inline-block; padding: 12px 24px; background: #007bff;
color: white; text-decoration: none; border-radius: 4px;">
View Order Details
</a>
</div>
`,
text: `Order Confirmation #${this.orderNumber}\n\nThank you for your order!\n\nTotal: $${this.total.toFixed(2)}`,
};
}
}Email with Attachments
import { readFileSync } from 'fs';
class InvoiceNotification extends Notification {
constructor(
private invoiceNumber: string,
private pdfPath: string,
) {
super();
}
via() {
return [NotificationChannel.EMAIL];
}
toEmail() {
return {
subject: `Invoice #${this.invoiceNumber}`,
html: '<p>Please find your invoice attached.</p>',
attachments: [
{
filename: `invoice-${this.invoiceNumber}.pdf`,
content: readFileSync(this.pdfPath),
},
],
};
}
}Custom From Address per Email
class CustomerSupportNotification extends Notification {
via() {
return [NotificationChannel.EMAIL];
}
toEmail() {
return {
from: {
email: '[email protected]',
name: 'Customer Support',
},
subject: 'Re: Your Support Ticket',
html: '<p>Thank you for contacting support...</p>',
};
}
}Reply-To and CC/BCC
class NewsletterNotification extends Notification {
via() {
return [NotificationChannel.EMAIL];
}
toEmail() {
return {
subject: 'Monthly Newsletter',
html: '<h1>This month in tech...</h1>',
replyTo: '[email protected]',
cc: ['[email protected]'],
bcc: ['[email protected]'],
};
}
}Email Templates
class PasswordResetNotification extends Notification {
constructor(
private userName: string,
private resetToken: string,
private resetUrl: string,
) {
super();
}
via() {
return [NotificationChannel.EMAIL];
}
toEmail() {
return {
subject: 'Reset Your Password',
html: this.getTemplate(),
text: this.getPlainText(),
};
}
private getTemplate(): string {
return `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.button {
display: inline-block;
padding: 12px 24px;
background: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h2>Password Reset Request</h2>
<p>Hi ${this.userName},</p>
<p>We received a request to reset your password. Click the button below to create a new password:</p>
<a href="${this.resetUrl}" class="button">Reset Password</a>
<p>This link will expire in 1 hour.</p>
<p>If you didn't request this, please ignore this email.</p>
</div>
</body>
</html>
`;
}
private getPlainText(): string {
return `
Password Reset Request
Hi ${this.userName},
We received a request to reset your password.
Click the link below to create a new password:
${this.resetUrl}
This link will expire in 1 hour.
If you didn't request this, please ignore this email.
`;
}
}Multi-Recipient Emails
class TeamUpdateNotification extends Notification {
constructor(private teamMembers: string[]) {
super();
}
via() {
return [NotificationChannel.EMAIL];
}
toEmail() {
return {
to: this.teamMembers.map((email) => ({ email })),
subject: 'Team Update',
html: '<h1>Important team update...</h1>',
};
}
}Error Handling
import { NotificationFailed } from 'townkrier-core';
eventDispatcher.on(NotificationFailed, async (event) => {
console.error('Email failed:', event.error.message);
// Handle specific errors
if (event.error.message.includes('Invalid email')) {
// Log invalid email for review
await logInvalidEmail(recipient);
} else if (event.error.message.includes('Rate limit')) {
// Implement rate limiting
await delayNextEmail();
}
});Common Errors
Invalid API key- Check your Resend API keyDomain not verified- Verify your sending domainInvalid email address- Validate recipient emailRate limit exceeded- Implement rate limiting
Testing
Development Mode
{
apiKey: process.env.RESEND_API_KEY,
from: '[email protected]',
debug: true, // Enable detailed logging
}Test Emails
Use test email addresses for development:
const testRecipient = {
[NotificationChannel.EMAIL]: {
email: '[email protected]', // Always succeeds
},
};Resend provides special test addresses:
[email protected]- Always delivers successfully[email protected]- Simulates a bounce[email protected]- Simulates a spam complaint
Best Practices
- Domain Verification: Always verify your sending domain
- Plain Text Fallback: Include both HTML and plain text versions
- Responsive Design: Use responsive HTML templates
- Unsubscribe Links: Include unsubscribe options in bulk emails
- Validate Emails: Validate recipient emails before sending
- Rate Limiting: Respect Resend's rate limits
- Error Handling: Implement proper error handling and logging
- Bounce Handling: Monitor bounces and remove invalid addresses
- Spam Compliance: Follow CAN-SPAM and GDPR guidelines
- Test Before Launch: Test emails on different clients and devices
Webhooks
Set up webhooks in Resend Dashboard to track:
- Email delivered
- Email opened
- Email clicked
- Email bounced
- Email complained
// Example webhook handler (Express)
app.post('/webhooks/resend', (req, res) => {
const { type, data } = req.body;
switch (type) {
case 'email.delivered':
// Update delivery status
break;
case 'email.bounced':
// Handle bounce, mark email as invalid
break;
case 'email.complained':
// Handle spam complaint, unsubscribe user
break;
}
res.status(200).send('OK');
});Pricing
Resend offers:
- Free Tier: 100 emails/day, 1 domain
- Paid Plans: Starting at $20/month for 50,000 emails
Check Resend Pricing for current rates.
Troubleshooting
"Domain not verified"
- Add DNS records provided by Resend
- Wait for DNS propagation (up to 48 hours)
- Check DNS records with
digor online tools
"Invalid API key"
- Verify API key is correct (no extra spaces)
- Check if key has necessary permissions
- Regenerate key if needed
Emails going to spam
- Verify SPF and DKIM records
- Warm up your domain gradually
- Avoid spam trigger words
- Include unsubscribe link
- Use a consistent from address
Slow delivery
- Check Resend status page
- Verify your rate limits
- Consider implementing queues for bulk sends
Rate Limits
Resend rate limits vary by plan:
- Free: Limited to bursts
- Paid: Higher limits based on plan
Implement queuing for bulk emails:
import { QueueManager } from 'townkrier-queue';
const queueManager = new QueueManager(queueAdapter, manager);
// Queue emails for gradual delivery
for (const user of users) {
await queueManager.enqueue(notification, {
[NotificationChannel.EMAIL]: {
email: user.email,
name: user.name,
},
});
}Related Packages
- townkrier-core - Core notification system
- townkrier-termii - SMS provider
- townkrier-fcm - Push notifications provider
- townkrier-queue - Queue system for background processing
- townkrier-dashboard - Monitoring dashboard
Examples
See the examples directory for complete working examples:
- Complete Example - Full multi-channel setup
- Email Examples - Email-specific examples
Resources
License
MIT
Support
Author
Jeremiah Olisa
