Building a Production-Ready Email System with AWS SES and Serverless Stack
Table of contents
Open Table of contents
Introduction
In today’s digital landscape, reliable email communication is a cornerstone for any modern application. Whether it’s for user notifications, password resets, or marketing campaigns, having a robust email system is crucial. Amazon Web Services Simple Email Service (AWS SES) offers a cost-effective, scalable solution for email delivery. Coupled with the power of the Serverless Stack (SST), developers can efficiently manage infrastructure while focusing on application logic.
In this blog post, we’ll explore how to build a production-ready email system using AWS SES and SST, leveraging serverless architecture to reduce operational overhead.
Introduction to AWS SES
AWS SES is a cloud-based email sending service designed to help digital marketers and application developers send marketing, notification, and transactional emails. Key features include:
- Scalability: Automatically scales to meet your email sending needs.
- Deliverability: High deliverability rates with ISP feedback loops and content filtering.
- Affordability: Pay-as-you-go pricing without upfront fees.
Detailed Breakdown of the Solution
Stack Setup
To get started, we’ll set up our stack using SST constructs. Below is the initial setup:
import { EventBus, StackContext } from "sst/constructs";
export function Events({ stack }: StackContext) {
const bus = new EventBus(stack, "event-bus", {
defaults: {
retries: 0,
},
});
bus.subscribe("send.email", {
handler: "packages/functions/src/events/send-email.handler",
logRetention: "one_day",
copyFiles: [{ from: "templates", to: "templates" }],
permissions: ["ses"],
});
return { bus };
}
Email handling
The send.email Event The send.email event is triggered whenever an email needs to be sent. It acts as a decoupled mechanism to handle email sending without blocking the main execution flow.
The sendEmail.handler Function
import { Emails } from "@email-service/core/email";
import { EventHandler } from "sst/node/event-bus";
import EmailService from "@email-service/core/emailService";
export const handler = EventHandler(Emails.Events.Send, async evt => {
console.info("email sending requested", evt);
const emailService = new EmailService();
console.log("Sending email", evt.properties);
await emailService.sendEmail(
[evt.properties.to],
evt.properties.from,
evt.properties.emailType,
evt.properties.subject,
evt.properties.data
);
console.info("Email sent successfully via EmailService");
});
Email Service Class
The EmailService class encapsulates the logic for sending emails using AWS SES.
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
import * as Handlebars from "handlebars";
import fs from "fs";
import path from "path";
class EmailService {
private sesClient: SESClient;
constructor() {
this.sesClient = new SESClient({});
}
public async compileTemplate(
templateName: string,
data: any
): Promise<string> {
const templatePath = path.join(
process.cwd(),
`templates/${templateName}.html`
);
const templateContent = fs.readFileSync(templatePath, "utf8");
const template = Handlebars.compile(templateContent);
return template(data);
}
public async sendEmail(
toAddress: string[],
fromAddress: string,
templateName: string,
subject: string,
data: any
): Promise<void> {
const htmlBody = await this.compileTemplate(templateName, {
...data,
});
const sendEmailCommand = new SendEmailCommand({
Destination: {
ToAddresses: toAddress,
},
Message: {
Body: {
Html: {
Charset: "UTF-8",
Data: htmlBody,
},
},
Subject: {
Charset: "UTF-8",
Data: subject,
},
},
Source: fromAddress,
});
try {
await this.sesClient.send(sendEmailCommand);
console.info("Email sent successfully");
} catch (error) {
console.error("Error sending email:", error);
throw error;
}
}
}
export default EmailService;
Pros of the solution
-
Scalability: Serverless architecture automatically scales with demand.
-
Manageability: SST simplifies infrastructure as code, making the system easier to maintain.
-
Reduced Operational Overhead: No need to manage servers or scaling policies manually.
-
Self-Contained Functions: All dependencies, including templates, are packaged together. Simplifies deployment and reduces external dependencies.
-
Improved Performance: Eliminates the need to fetch templates from external sources at runtime. Reduces latency, enhancing user experience.
-
Version Control and Consistency: Templates are versioned alongside your code. Ensures consistency between code changes and email content.
-
Ease of Development and Maintenance: Developers can modify templates without affecting external resources. Simplifies the process of updating email content.
-
Security Benefits: Reduces the attack surface by minimizing external calls. Templates are stored securely within the Lambda package.
-
Convention Over Configuration: Establishing a naming convention for templates simplifies code. Reduces the likelihood of errors due to misnamed templates.
Demo !!
CD to the repository you cloned and run
pnpm dev
✔ Deployed:
Events
API
ApiEndpoint: https://o98egbtw09.execute-api.eu-west-1.amazonaws.com
Now call the API to send an email
And here is the preview of the received email
Conclusion
Embedding email templates within your Lambda functions using AWS SES and SST provides a robust, scalable, and efficient email system. This methodology enhances performance, simplifies deployment, and improves maintainability by keeping your templates version-controlled and packaged with your code.
Call to Action
Ready to enhance your application’s email capabilities? Clone the sample repository and start building your production-ready email system with AWS SES and SST today!
Note:The code snippets provided are for educational purposes. Ensure you customize configurations such as AWS regions, email addresses, and template paths to suit your application’s requirements. Ensure that:
- Your SES service is production enabled and not in sandbox
- DMARC, SPF and DKIM records are configured
What’s next
The sample repository is simplified to focus on emailing features but It lacks many others like:
- type safety: we could add strong typing to EmailService by introducing the use of libraries like zod
- Adding circuit breaker pattern to prevent banning your SES service for too many bounced emails ( this could be a subject of future article, stay tuned 🙂 )
- AWS lambda deployment package is limited to 250 MB (unzipped), as an alternative to storing email templates, we could use JSX EMAIL