Setting Up Email Service with SendGrid and Node.js
Back to blog
emailsendgridnodejstypescriptdevops

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.

February 18, 2025
5 min read

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

  1. Go to SendGrid Dashboard → Dynamic Templates
  2. 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>
  1. 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>
  1. 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 TypeUse CaseTemplate Type
WelcomeNew user registrationHTML template
VerificationEmail confirmationOne-time code
Password ResetAccount recoveryMagic link
NotificationUser alertsDynamic content
NewsletterMarketing emailsBulk 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

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.