How I Built a Contact Form with AWS CDK, API Gateway, Lambda, and SNS for SMS and Email Notifications
In this article, I share how I built an integrated contact form for my portfolio website that notifies me via SMS and email whenever someone reaches out. Using AWS CDK for infrastructure as code, API Gateway for handling requests, Lambda for processing logic, and SNS for notifications, I created a robust system that enhances my availability to my audience.
Goal of the Project
The primary goal was to implement a contact form capable of sending notifications through both SMS and email. This form allows users to easily communicate with me, ensuring I can respond promptly to inquiries, feedback, or opportunities.
Step-by-Step Implementation
Step 1: Setting Up the AWS CDK Environment
First, I set up the AWS Cloud Development Kit (CDK) environment to define my infrastructure as code, making it reproducible and easy to manage.
# Initialize a new CDK project
cdk init app --language typescript
Note that if it’s your first time using CDK, you might need to use cdk boostrap
Step 2: Creating the API Gateway, SNS topic and Lambda Function
I used AWS API Gateway to create an HTTP endpoint that triggers a Lambda function upon form submission. The responsibility of the lambda function is to send the form data to the SNS topic.
Here’s how I did it:
// Import libraries
const { Stack, CfnOutput } = require('aws-cdk-lib');
const { Function, Runtime, Code } = require('aws-cdk-lib/aws-lambda');
const { RestApi, LambdaIntegration, Cors } = require('aws-cdk-lib/aws-apigateway');
const { PolicyStatement, Policy } = require('aws-cdk-lib/aws-iam');
const { Topic } = require('aws-cdk-lib/aws-sns');
const cdk = require('aws-cdk-lib');
class ApiStack extends Stack {
constructor(scope, id, props) {
super(scope, id, props);
// Define the API Gateway
const api = new RestApi(this, 'ApiStack', {
restApiName: 'ApiStack',
defaultCorsPreflightOptions: {
allowOrigins: Cors.ALL_ORIGINS, // Adjust this to a more restrictive setting for production
allowMethods: Cors.ALL_METHODS,
allowHeaders: Cors.DEFAULT_HEADERS,
}
});
// Create the SNS Topic
const snsTopic = new Topic(this, 'EmailNotificationTopic', {
topicName: 'EmailNotificationTopic'
});
// Create the SendNotification Lambda function. This function is responsible for sending the form data to the SNS topic
const sendNotificationLambda = new Function(this, 'SendNotificationHandler', {
runtime: Runtime.NODEJS_16_X,
code: Code.fromAsset('lambda'),
handler: 'sendNotification.handler',
environment: {
SNS_TOPIC_ARN: snsTopic.topicArn
}
});
// The lambda function needs to be allowed to publish messages to the SNS topic
sendNotificationLambda.addToRolePolicy(new PolicyStatement({
actions: ['sns:Publish'],
resources: [snsTopic.topicArn]
}));
// Create a /send-email POST endpoint
const sendEmailResource = api.root.addResource('send-email');
sendEmailResource.addMethod('POST', new LambdaIntegration(sendNotificationLambda));
// Create Output that I can reuse in other Cloudformation stacks in the future
new CfnOutput(this, 'ApiId', {
value: api.restApiId,
description: 'The ID of the API',
exportName: 'ApiId',
});
// output the rootResourceId
new CfnOutput(this, 'RootResourceId', {
value: api.restApiRootResourceId,
description: 'The ID of the root resource',
exportName: 'RootResourceId',
});
// Output the SNS Topic ARN
new CfnOutput(this, 'SNSTopicArn', {
value: snsTopic.topicArn,
description: 'ARN of the SNS Topic',
exportName: 'SNSTopicArn'
});
}
}
module.exports = { PortfolioApplicationApiStack }
Step 3: Configuring the SNS Topic
Next, I set up the SNS topic to dispatch notifications to my phone and email.
const { LambdaSubscription, SmsSubscription} = require('aws-cdk-lib/aws-sns-subscriptions');
class ApiStack extends Stack {
constructor(scope, id, props) {
super(scope, id, props);
}
/* Existing code */
// Create a lambda function that's responsible to send email via SES.
const sendEmailLambda = new Function(this, 'SendEmailHandler', {
runtime: Runtime.NODEJS_16_X, // Choose the runtime for your Lambda function
code: Code.fromAsset('lambda'), // The directory where your Lambda code is located
handler: 'sendEmail.handler', // Your handler file and function
});
// Provide the lambda function with the right permissions to send emails via SES
sendEmailLambda.role.attachInlinePolicy(new Policy(this, 'SendEmailPolicy', {
statements: [
new PolicyStatement({
actions: [
'ses:SendRawEmail',
'ses:SendEmail'
],
resources: ['*']
})
]
}));
// Create SMS and Lambda SNS subscriptions
snsTopic.addSubscription(new LambdaSubscription(sendEmailLambda));
const smsPhoneNumber = '+1-XXX-XXX-XXX'; // e.g. Pull the phone number from ssm parameter store
snsTopic.addSubscription(new SmsSubscription(smsPhoneNumber));
}
Step 4: Implement the code for the Lambda functions
I implemented the sendEmail function:
// Load the AWS SDK for Node.js
const AWS = require('aws-sdk');
// Set the region
AWS.config.update({region: 'us-east-1'});
// Create SES service object
const ses = new AWS.SES({apiVersion: '2010-12-01'});
exports.handler = async (event) => {
const parsedObject = JSON.parse(event.Records[0].Sns.Message);
const { to, subject, body, name, email } = parsedObject;
// Email parameters
const params = {
Destination: {
ToAddresses: [to]
},
Message: {
Body: {
Text: {
Data: `You've received a new message. From: ${name} (${email}) - message: ${body}`
}
},
Subject: {
Data: subject
}
},
Source: 'app@sofianeouafir.com'
};
// Try to send the email
try {
const data = await ses.sendEmail(params).promise();
// Return a successful response
return {
statusCode: 200, // HTTP Status code
body: JSON.stringify({
message: 'Email sent!',
data
}),
headers: {
'Content-Type': 'application/json',
}
};
// Catch any errors
} catch (error) {
console.log(error);
// Return an error response
return {
statusCode: 500, // Or another appropriate server error code
body: JSON.stringify({
message: 'Error sending email',
error: error.message // Providing error message
}),
headers: {
'Content-Type': 'application/json'
}
};
}
};
And the sendNotification lambda function
const AWS = require('aws-sdk');
// Set the region
AWS.config.update({region: 'us-east-1'});
const sns = new AWS.SNS();
// sendNotification.handler
exports.handler = async (event) => {
try {
const message = JSON.stringify(event.body); // assuming the body of the request is the message to be sent
const params = {
Message: message,
TopicArn: process.env.SNS_TOPIC_ARN
};
const data = await sns.publish(params).promise();
return {
statusCode: 200,
body: JSON.stringify({ message: 'Message sent to SNS', messageId: data.MessageId }),
headers: {
'Content-Type': 'application/json',
}
};
} catch (err) {
console.error(err);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Failed to send message' }),
headers: {
'Content-Type': 'application/json',
}
};
}
};
Step 5: Deploying the CDK Stack
Finally, deploy the CDK stack to provision all resources in AWS.
cdk deploy
Step 6: Integrate the form with the API gateway
I’ve simply integrated the form with the API gateway using the axios library. Make sure the CORS configuration are properly set up.
await axios.post('https://api.sofianeouafir.com/send-email', {
name,
to,
subject,
body,
email
});
Conclusion
By leveraging AWS CDK, API Gateway, Lambda, and SNS, I created an effective notification system for my portfolio’s contact form. This setup not only notifies me promptly via SMS and email but also serves as a reliable communication bridge between my visitors and me.