Setting Up Email Service with SendGrid and Node.js
Complete guide to integrating SendGrid for transactional emails, templates, and email verification in Node.js applications with TypeScript.
Setting Up Email Service with SendGrid and Node.js
Email is essential for user engagement, verification, and notifications. SendGrid provides a reliable, scalable email service with templates, tracking, and deliverability optimization built-in.
Why SendGrid?
SendGrid handles SMTP infrastructure, spam filtering, bounce management, and email tracking. With 99.99% uptime and powerful templates, it eliminates email infrastructure headaches while maintaining high deliverability rates.
Prerequisites
- Node.js 18+ with TypeScript
- SendGrid account (free tier available)
- Email template knowledge (optional)
- Express.js API set up
Step 1: Install SendGrid SDK
npm install @sendgrid/mail @sendgrid/client
npm install -D @types/node
Step 2: Create SendGrid Configuration
Create src/config/sendgrid.ts:
import sgMail from '@sendgrid/mail';
const SENDGRID_API_KEY = process.env.SENDGRID_API_KEY;
const FROM_EMAIL = process.env.FROM_EMAIL || 'noreply@example.com';
const FROM_NAME = process.env.FROM_NAME || 'Your App Name';
if (!SENDGRID_API_KEY) {
throw new Error('SENDGRID_API_KEY environment variable is required');
}
sgMail.setApiKey(SENDGRID_API_KEY);
export const sendgridConfig = {
apiKey: SENDGRID_API_KEY,
fromEmail: FROM_EMAIL,
fromName: FROM_NAME,
};
export default sgMail;
Step 3: Set Environment Variables
Add to .env:
SENDGRID_API_KEY=your-sendgrid-api-key-here
FROM_EMAIL=noreply@yourdomain.com
FROM_NAME=Your App Name
# Optional: Template IDs from SendGrid dashboard
WELCOME_EMAIL_TEMPLATE_ID=d-template-id-here
VERIFICATION_EMAIL_TEMPLATE_ID=d-template-id-here
PASSWORD_RESET_TEMPLATE_ID=d-template-id-here
NEWSLETTER_TEMPLATE_ID=d-template-id-here
Step 4: Create Email Type Definitions
Create src/types/email.ts:
export interface EmailRecipient {
email: string;
name?: string;
}
export interface EmailAttachment {
content: string; // Base64 encoded
filename: string;
type: string;
disposition?: string;
}
export interface SendEmailOptions {
to: EmailRecipient | EmailRecipient[];
subject: string;
htmlContent?: string;
textContent?: string;
templateId?: string;
dynamicData?: Record<string, any>;
cc?: EmailRecipient[];
bcc?: EmailRecipient[];
replyTo?: string;
attachments?: EmailAttachment[];
tags?: string[];
unsubscribeGroupId?: number;
}
export interface EmailTemplate {
templateId: string;
dynamicData: Record<string, any>;
}
export interface EmailLog {
id: string;
to: string;
subject: string;
status: 'sent' | 'failed' | 'bounced' | 'delivered';
createdAt: Date;
messageId?: string;
}
Step 5: Create Email Service
Create src/services/emailService.ts:
import sgMail from '../config/sendgrid';
import { sendgridConfig } from '../config/sendgrid';
import { SendEmailOptions, EmailRecipient } from '../types/email';
function normalizeRecipient(recipient: EmailRecipient | string): EmailRecipient {
if (typeof recipient === 'string') {
return { email: recipient };
}
return recipient;
}
function normalizeRecipients(
recipients: EmailRecipient | EmailRecipient[] | string | string[]
): EmailRecipient[] {
const arr = Array.isArray(recipients) ? recipients : [recipients];
return arr.map(normalizeRecipient);
}
export async function sendEmail(options: SendEmailOptions): Promise<string> {
try {
const toRecipients = normalizeRecipients(options.to);
const message: any = {
from: {
email: sendgridConfig.fromEmail,
name: sendgridConfig.fromName,
},
personalizations: [
{
to: toRecipients.map((recipient) => ({
email: recipient.email,
name: recipient.name,
})),
dynamicTemplateData: options.dynamicData || {},
},
],
};
if (options.templateId) {
message.templateId = options.templateId;
} else {
message.subject = options.subject;
if (options.htmlContent) {
message.content = [{ type: 'text/html', value: options.htmlContent }];
} else if (options.textContent) {
message.content = [{ type: 'text/plain', value: options.textContent }];
}
}
if (options.cc) {
message.personalizations[0].cc = normalizeRecipients(options.cc).map((r) => ({
email: r.email,
name: r.name,
}));
}
if (options.bcc) {
message.personalizations[0].bcc = normalizeRecipients(options.bcc).map((r) => ({
email: r.email,
name: r.name,
}));
}
if (options.replyTo) {
message.replyTo = options.replyTo;
}
if (options.attachments) {
message.attachments = options.attachments;
}
if (options.tags) {
message.personalizations[0].customArgs = {
tags: options.tags.join(','),
};
}
if (options.unsubscribeGroupId) {
message.asm = {
groupId: options.unsubscribeGroupId,
};
}
const response = await sgMail.send(message);
const messageId = response[0].headers['x-message-id'];
console.log(`Email sent successfully. Message ID: ${messageId}`);
return messageId;
} catch (error) {
console.error('Error sending email:', error);
throw error;
}
}
export async function sendBulkEmail(
recipients: EmailRecipient[],
options: Omit<SendEmailOptions, 'to'>
): Promise<string[]> {
const messageIds: string[] = [];
for (const recipient of recipients) {
try {
const messageId = await sendEmail({
...options,
to: recipient,
});
messageIds.push(messageId);
} catch (error) {
console.error(`Failed to send email to ${recipient.email}:`, error);
}
}
return messageIds;
}
export async function sendEmailWithTemplate(
to: EmailRecipient | EmailRecipient[],
templateId: string,
dynamicData: Record<string, any>
): Promise<string> {
return sendEmail({
to,
templateId,
dynamicData,
});
}
export async function scheduleEmail(
options: SendEmailOptions,
sendAt: Date
): Promise<string> {
const message: any = {
from: {
email: sendgridConfig.fromEmail,
name: sendgridConfig.fromName,
},
to: normalizeRecipients(options.to).map((r) => ({
email: r.email,
name: r.name,
})),
subject: options.subject,
html: options.htmlContent,
sendAt: Math.floor(sendAt.getTime() / 1000),
};
try {
const response = await sgMail.send(message);
return response[0].headers['x-message-id'];
} catch (error) {
console.error('Error scheduling email:', error);
throw error;
}
}
Step 6: Create Templated Email Helpers
Create src/services/emailTemplates.ts:
import { sendEmailWithTemplate } from './emailService';
import { EmailRecipient } from '../types/email';
const TEMPLATES = {
WELCOME: process.env.WELCOME_EMAIL_TEMPLATE_ID,
VERIFICATION: process.env.VERIFICATION_EMAIL_TEMPLATE_ID,
PASSWORD_RESET: process.env.PASSWORD_RESET_TEMPLATE_ID,
NEWSLETTER: process.env.NEWSLETTER_TEMPLATE_ID,
};
export async function sendWelcomeEmail(
recipient: EmailRecipient,
userName: string
): Promise<string> {
if (!TEMPLATES.WELCOME) {
throw new Error('Welcome email template not configured');
}
return sendEmailWithTemplate(recipient, TEMPLATES.WELCOME, {
userName,
currentYear: new Date().getFullYear(),
});
}
export async function sendVerificationEmail(
recipient: EmailRecipient,
verificationCode: string,
verificationUrl: string
): Promise<string> {
if (!TEMPLATES.VERIFICATION) {
throw new Error('Verification email template not configured');
}
return sendEmailWithTemplate(recipient, TEMPLATES.VERIFICATION, {
verificationCode,
verificationUrl,
expirationMinutes: 60,
});
}
export async function sendPasswordResetEmail(
recipient: EmailRecipient,
resetUrl: string,
userName: string
): Promise<string> {
if (!TEMPLATES.PASSWORD_RESET) {
throw new Error('Password reset email template not configured');
}
return sendEmailWithTemplate(recipient, TEMPLATES.PASSWORD_RESET, {
resetUrl,
userName,
expirationHours: 24,
});
}
export async function sendNewsletterEmail(
recipients: EmailRecipient[],
title: string,
content: string
): Promise<string[]> {
if (!TEMPLATES.NEWSLETTER) {
throw new Error('Newsletter email template not configured');
}
const messageIds: string[] = [];
for (const recipient of recipients) {
const messageId = await sendEmailWithTemplate(
recipient,
TEMPLATES.NEWSLETTER,
{
title,
content,
unsubscribeUrl: `https://yourdomain.com/unsubscribe?email=${recipient.email}`,
}
);
messageIds.push(messageId);
}
return messageIds;
}
export async function sendInvoiceEmail(
recipient: EmailRecipient,
invoiceNumber: string,
amount: number,
attachmentContent: string
): Promise<string> {
// Send with attachment
const { sendEmail } = await import('./emailService');
return sendEmail({
to: recipient,
subject: `Invoice #${invoiceNumber}`,
htmlContent: `
<h1>Invoice #${invoiceNumber}</h1>
<p>Amount Due: $${amount.toFixed(2)}</p>
<p>Please see the attached invoice for details.</p>
`,
attachments: [
{
content: Buffer.from(attachmentContent).toString('base64'),
filename: `invoice-${invoiceNumber}.pdf`,
type: 'application/pdf',
},
],
});
}
Step 7: Create Email Routes
Create src/routes/email.ts:
import express, { Request, Response } from 'express';
import { authenticateToken } from '../middleware/auth';
import {
sendWelcomeEmail,
sendVerificationEmail,
sendPasswordResetEmail,
} from '../services/emailTemplates';
import { sendEmail } from '../services/emailService';
const router = express.Router();
// Test email endpoint
router.post('/test', async (req: Request, res: Response) => {
try {
const { email } = req.body;
const messageId = await sendEmail({
to: email,
subject: 'Test Email',
htmlContent: '<h1>This is a test email</h1><p>If you received this, SendGrid is working!</p>',
});
res.json({
message: 'Test email sent',
messageId,
});
} catch (error) {
res.status(500).json({
error: error instanceof Error ? error.message : 'Failed to send test email',
});
}
});
// Send verification email
router.post('/verify', async (req: Request, res: Response) => {
try {
const { email, verificationCode } = req.body;
const messageId = await sendVerificationEmail(
{ email },
verificationCode,
`https://yourdomain.com/verify?code=${verificationCode}`
);
res.json({
message: 'Verification email sent',
messageId,
});
} catch (error) {
res.status(500).json({
error: error instanceof Error ? error.message : 'Failed to send verification email',
});
}
});
// Send password reset email
router.post('/password-reset', async (req: Request, res: Response) => {
try {
const { email, resetToken, userName } = req.body;
const messageId = await sendPasswordResetEmail(
{ email },
`https://yourdomain.com/reset-password?token=${resetToken}`,
userName
);
res.json({
message: 'Password reset email sent',
messageId,
});
} catch (error) {
res.status(500).json({
error: error instanceof Error ? error.message : 'Failed to send reset email',
});
}
});
export default router;
Step 8: Configure Email Templates in SendGrid
- Go to SendGrid Dashboard → Dynamic Templates
- Create new template for Welcome email:
<h1>Welcome, {{userName}}!</h1>
<p>Thank you for joining us.</p>
<a href="https://yourdomain.com/dashboard">Get Started</a>
<footer>© {{currentYear}} Your Company</footer>
- Create Verification template:
<h1>Verify Your Email</h1>
<p>Your verification code is: <strong>{{verificationCode}}</strong></p>
<p>Or <a href="{{verificationUrl}}">click here to verify</a></p>
<p>This code expires in {{expirationMinutes}} minutes.</p>
- Create Password Reset template:
<h1>Reset Your Password</h1>
<p>Hi {{userName}},</p>
<p><a href="{{resetUrl}}">Click here to reset your password</a></p>
<p>This link expires in {{expirationHours}} hours.</p>
<p>If you didn't request this, please ignore this email.</p>
Step 9: Track Email Events
Create src/services/emailTracking.ts:
import { Request, Response } from 'express';
import crypto from 'crypto';
// Verify SendGrid webhook signature
export function verifyWebhookSignature(
req: Request,
publicKey: string
): boolean {
const signature = req.get('X-Twilio-Email-Event-Webhook-Signature');
const timestamp = req.get('X-Twilio-Email-Event-Webhook-Timestamp');
const body = req.rawBody || JSON.stringify(req.body);
if (!signature || !timestamp) {
return false;
}
const signedContent = `${timestamp}${body}`;
const hash = crypto
.createHash('sha256')
.update(signedContent)
.digest('base64');
return hash === signature;
}
export async function handleEmailWebhook(req: Request, res: Response) {
const events = req.body;
for (const event of events) {
switch (event.event) {
case 'delivered':
console.log(`Email delivered: ${event.email}`);
// Update database
break;
case 'opened':
console.log(`Email opened: ${event.email}`);
// Track engagement
break;
case 'clicked':
console.log(`Email link clicked: ${event.email}`);
// Track link clicks
break;
case 'bounce':
console.log(`Email bounced: ${event.email}`);
// Handle bounce
break;
case 'unsubscribe':
console.log(`User unsubscribed: ${event.email}`);
// Update subscription status
break;
case 'spamreport':
console.log(`Spam report: ${event.email}`);
// Handle spam report
break;
}
}
res.json({ success: true });
}
Step 10: Create Email Queue System
For better reliability, implement email queuing with Bull:
npm install bull
Create src/services/emailQueue.ts:
import Bull from 'bull';
import { sendEmail } from './emailService';
import { SendEmailOptions } from '../types/email';
const emailQueue = new Bull('email', {
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
},
});
// Process email jobs
emailQueue.process(async (job) => {
try {
const result = await sendEmail(job.data);
return result;
} catch (error) {
throw error;
}
});
// Handle job completion
emailQueue.on('completed', (job) => {
console.log(`Email job ${job.id} completed`);
});
// Handle job failures
emailQueue.on('failed', (job, error) => {
console.error(`Email job ${job.id} failed:`, error);
});
export async function queueEmail(options: SendEmailOptions) {
await emailQueue.add(options, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000,
},
});
}
| Email Type | Use Case | Template Type |
|---|---|---|
| Welcome | New user registration | HTML template |
| Verification | Email confirmation | One-time code |
| Password Reset | Account recovery | Magic link |
| Notification | User alerts | Dynamic content |
| Newsletter | Marketing emails | Bulk send |
Best Practices
Avoid Spam Folder: Maintain domain reputation
- Implement SPF, DKIM, DMARC records
- Use reply-to addresses
- Monitor bounce rates
Personalization: Use dynamic template data
const messageId = await sendEmail({
to: { email: user.email, name: user.name },
templateId: 'd-template-id',
dynamicData: { userName: user.name },
});
Unsubscribe Management: Always provide unsubscribe option
const message = {
asm: { groupId: 12345 }, // Unsubscribe group
};
Monitor Metrics: Track delivery, opens, clicks
- Check SendGrid dashboard for metrics
- Set up webhook for real-time events
- Monitor bounce rates
Testing Email Locally
Use Ethereal Email for testing:
import nodemailer from 'nodemailer';
if (process.env.NODE_ENV === 'development') {
const testAccount = await nodemailer.createTestAccount();
// Use testAccount credentials for testing
}
Useful Resources
- SendGrid API Documentation
- SendGrid Node.js Library
- Email Template Best Practices
- Deliverability Tips
Conclusion
You've integrated professional email capabilities with SendGrid, enabling transactional emails, templates, and tracking. Implement queuing for reliability, monitor delivery metrics, and maintain sender reputation for maximum deliverability.