In this tutorial you will learn how to set up a form handler in AWS Lambda with and without attachments.
Introduction
We’re going to set up two Lambda functions that send out form data via an external Email server, you could also use SES instead of the external Email server if desired. The first will handle a simple application/json
post request, and the second will handle multipart/form-data
requests with file attachments.
Getting Started
First, we need to install serverless
globally with the command npm install --global serverless
, then we can create a new project with the command below, we’ll be using NodeJS for this example.
serverless create --template aws-nodejs --path form-handler-demo
After moving into the new project folder (cd form-handler-demo
), you’ll want to initialize the package.json
file with the command npm init -y
, and then add the offline package so we can run the function locally with the command npm install --save-dev serverless-offline
.
Let’s start simple and handle a basic form sent over as application/json
, which is dirt simple to do with window.fetch
on the front-end site, for example:
fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
}).then(
function (response) {
response.status; //=> number 100–599
response.statusText; //=> String
response.headers; //=> Headers
response.url; //=> String
return response.text();
},
function (error) {
error.message; //=> String
},
);
NodeMailer
We’re going to need nodemailer
so we can send Emails out.
npm install nodemailer
We’ll configure nodemailer
to use the credentials we’ll provide later in environment variables.
src/mailer/transport.js
const mailer = require('nodemailer');
module.exports = mailer.createTransport({
host: process.env.MAIL_SERVER,
port: process.env.MAIL_PORT,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASSWORD,
},
});
And a simple function to actually send the Email.
src/mailer/sendMail.js
const transport = require('./transport');
module.exports = async (message) => {
const result = new Promise((resolve, reject) => {
transport.sendMail(message, (err) => {
if (err) {
reject(err);
} else {
resolve(true);
}
});
}).catch((err) => {
console.log(err);
return false;
});
return await result;
};
Optionally but recommended is Google ReCAPTCHA to fend off the bots.
npm install google-recaptcha
Next we need to validate the ReCAPTCHA (note that we don’t catch the error here, we want the function to error out if validation fails), and format the form data into a new Email and reciept.
We’ll pass the ReCAPTCHA verifier function verfier
to this module from it’s parent
src/message-generator/json.js
module.exports = async ({ event, emailFrom, emailTo, verifier }) => {
const {
firstName,
lastName,
email: userEmail,
message,
recaptcha,
} = JSON.parse(event.body);
await new Promise((resolve, reject) => {
verifier.verify({ response: recaptcha }, (err, data) => {
const { success } = data;
if (err || !success) {
reject('reCAPTCHA validation failed');
}
resolve();
});
});
const messageBody = `
First Name: ${firstName}
Last Name: ${lastName}
Email: ${userEmail}
Message:
${message}
`;
const receiptBody = `
Hi ${firstName},
Thanks for reaching out, I'll be in touch soon!
`;
const email = {
to: emailTo,
from: emailFrom,
replyTo: `${firstName} ${lastName} <${userEmail}>`,
subject: 'Contact Form',
text: messageBody,
};
const receipt = {
to: userEmail,
from: emailFrom,
subject: 'Contact Form',
text: receiptBody,
};
return { email, receipt };
};
Side note: If you want to get fancy, you can create an HTML Email using a string template literal, for example:
src/message-generator/templates/receipt.js
// NOTE : This is stripped down to save space,
// you should search the web for an HTML Email
// boilerplate/template to get started!
const emailReceiptHTML = ({ firstName }) => `
<!DOCTYPE html>
<html>
<head>
// Add styles here!
</head>
<body>
// Add styled divs here!
Hi ${firstName},
Thanks for reaching out, I'll be in touch soon!
</body>
</html>
`;
module.exports = emailRecieptHTML;
src/message-generator/json.js
const emailRecieptHTML = require('./templates/receipt');
module.exports = async ({ event, emailFrom, emailTo, verifier }) => {
const {
firstName,
lastName,
email: userEmail,
message,
recaptcha,
} = JSON.parse(event.body);
await new Promise((resolve, reject) => {
verifier.verify({ response: recaptcha }, (err, data) => {
const { success } = data;
if (err || !success) {
reject('reCAPTCHA validation failed');
}
resolve();
});
});
const messageBody = `
First Name: ${firstName}
Last Name: ${lastName}
Email: ${userEmail}
Message:
${message}
`;
const receiptBody = `
Hi ${firstName},
Thanks for reaching out, I'll be in touch soon!
`;
const html = emailReceiptHTML({ firstName });
const email = {
to: emailTo,
from: emailFrom,
replyTo: `${firstName} ${lastName} <${userEmail}>`,
subject: 'Contact Form',
text: messageBody,
};
const receipt = {
to: userEmail,
from: emailFrom,
subject: 'Contact Form',
html,
text: receiptBody,
};
return { email, receipt };
};
Moving on, we’ll write a wrapper function here so we can add more forms later, and to supply the ReCAPTCHA verifier.
src/message-generator/index.js
const googleRecaptcha = require('google-recaptcha');
const json = require('./json');
const {
EMAIL_TO: emailTo,
EMAIL_FROM: emailFrom,
RECAPTCHA_SECRET,
} = process.env;
const verifier = new googleRecaptcha({ secret: RECAPTCHA_SECRET });
module.exports.json = async (event) => {
return await json({
event,
emailTo,
emailFrom,
verifier,
});
};
To keep things tidy, we’ll generate our responses to the function in a separate file, since they can be a bit verbose.
src/response-generator.js
module.exports.success = (event) => ({
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(
{
message: 'Form submitted successfully',
success: true,
input: event,
},
null,
2,
),
});
module.exports.error = (event) => ({
statusCode: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(
{
message: 'Server Error, please try again later',
success: false,
input: event,
},
null,
2,
),
});
Entrypoint
serverless
should have generated an example handler.js
file, go ahead and delete that, we’ll create a new one in the src
directory. This is the entrypoint for the function, and we’ll specify that later in the serverless.yml
file.
If you aren’t familiar with ternary operators yet, the syntax is expression ? if true do this : else do this
src/handler.js
require('dotenv').config();
const sendmail = require('./mailer/sendmail');
const messageGenerator = require('./message-generator');
const responseGenerator = require('./response-generator');
module.exports.json = async (event) => {
try {
const { email, receipt } = await messageGenerator.json(event);
const emailResult = await sendmail(email);
const receiptResult = await sendmail(receipt);
return !!emailResult && !!receiptResult
? responseGenerator.success(event)
: responseGenerator.error(event);
} catch (err) {
console.log(err);
return responseGenerator.error(event);
}
};
We’ll need to install the dotenv
package and create a .env
file to test locally with our Email server credentials and ReCAPTCHA secret.
If you don’t have a ReCAPTCHA key/secret, you’ll need to generate them from Google.com/ReCAPTCHA
npm install dotenv
.env
EMAIL_TO=
EMAIL_FROM=
MAIL_SERVER=
MAIL_PORT=
MAIL_USER=
MAIL_PASSWORD=
RECAPTCHA_SECRET=
Don’t forget to add .env
to your .gitignore
file! I also like to add a .env.example
file with the values removed so it’s clear what values are required.
Test Locally
Let’s configure serverless for local testing now.
serverless.yml
plugins:
- serverless-offline
service: form-handler-demo
provider:
name: aws
runtime: nodejs12.x
region: us-west-1
package:
exclude:
- .gitiginore
- .env
- package.json
- package-lock.json
- serverless.yml
functions:
json:
handler: src/handler.json
events:
- http:
path: /json
method: post
cors:
origin: '*'
headers:
- Content-Type
Note the region line above, I just picked the closest one to me, there are many options to choose if you log in to AWS Management Console and click the drop down menu in the upper right for regions.
Now we can use the command serverless offline
to run our function locally.
Let’s create a file to test this called test-form.html
.
Don’t forget to add your ReCAPTCHA site key in this file, make sure you add localhost
to the allowed hosts in the ReCAPTCHA admin console as well.
This looks over-complicated for a plain HTML file, but in React I find application/json
submissions to be an easy way to handle forms when you want to manually manage the data going out.
<!DOCTYPE html>
<html>
<head>
<script
src="https://www.google.com/recaptcha/api.js"
async
defer
></script>
<style>
form {
display: flex;
flex-direction: column;
width: 304px;
margin: 2rem auto;
}
input {
margin-bottom: 1rem;
}
button {
margin-top: 1rem;
}
</style>
</head>
<form id="form">
<label for="firstName">First Name</label>
<input
type="text"
name="firstName"
/>
<label for="lastName">Last Name</label>
<input
type="text"
name="lastName"
/>
<label for="email">Email Address</label>
<input
type="text"
name="email"
/>
<label for="message">message</label>
<input
type="text"
name="message"
/>
<div
id="recaptcha"
class="g-recaptcha"
data-sitekey="REPLACE THIS WITH YOUR SITE KEY!"
></div>
<button type="submit">Submit</button>
</form>
<script>
const form = document.getElementById('form');
form.addEventListener('submit', submitForm, true);
function submitForm(event) {
event.preventDefault();
const formData = new FormData(form);
const data = {
firstName: formData.get('firstName'),
lastName: formData.get('lastName'),
email: formData.get('email'),
message: formData.get('message'),
recaptcha: grecaptcha.getResponse(),
};
fetch('http://localhost:3000/dev/json', {
method: 'POST',
mode: 'cors',
body: JSON.stringify(data),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
credentials: 'same-origin',
}).then(
function (response) {
console.log(response.statusText);
},
function (error) {
console.error(error.message);
},
);
}
</script>
</html>
We can run npx serve test-form.html
, and navigate to localhost:5000
in a browser to test the form out.
Deploy to AWS
Before we deploy, we need to add our environment variables to the serverless.yml
file, and like the .env
file copy it to serverless.example.yml
with the credentials removed to keep them out of any git repository.
Don’t forget to add serverless.yml
to your .gitignore
file!
serverless.yml
plugins:
- serverless-offline
service: form-handler-demo
provider:
name: aws
runtime: nodejs12.x
region: us-west-1
environment:
EMAIL_TO: REPLACE_THIS_WITH_EMAIL_ADDRESS
EMAIL_FROM: REPLACE_THIS_WITH_EMAIL_ADDRESS
MAIL_SERVER: REPLACE_THIS_WITH_EMAIL_SERVER
MAIL_PORT: REPLACE_THIS_WITH_EMAIL_SERVER_PORT
MAIL_USER: REPLACE_THIS_WITH_EMAIL_API_KEY_ID
MAIL_PASSWORD: REPLACE_THIS_WITH_EMAIL_API_KEY_SECRET
RECAPTCHA_SECRET: REPLACE_THIS_WITH_RECAPTCHA_SECRET
package:
exclude:
- .gitiginore
- .env
- .env.example
- package.json
- package-lock.json
- serverless.yml
- serverless.example.yml
functions:
json:
handler: src/handler.json
events:
- http:
path: /json
method: post
cors:
origin: '*'
headers:
- Content-Type
You’ll need to log in to aws.amazon.com , head to “My Account” > “AWS Management Console”, then click on your username in the top right menu, then “Users” on the left-hand menu.
Click on the “Add user” button in the top left, I’ll call mine “serverless”, and select “Programmatic access”.
Next, click on “Create group”, which I’ll also call “serverless”, then use the search bar to find and check these policies:
AWSCodeDeployRoleForLambda
AWSCloudFormationFullAccess
AWSLambdaFullAccess
AmazonAPIGatewayAdministrator
IAMFullAccess
You may add tags if you wish in the next step, these are just for you to help manage multiple users.
When you get to the success page, you can now authorize serverless
on your workstation to push to your AWS account with the following command.
serverless config credentials --provider aws --key YOUR_KEY_ID --secret YOUR_SECRET_KEY
Let’s add some NPM scripts to the package.json
file to make things easier to remember.
"scripts": {
"start": "serverless offline",
"deploy:dev": "serverless deploy --stage development",
"deploy:prod": "serverless deploy --stage production"
}
Now we can run npm run deploy:dev
to push to a “development” stage on AWS, serverless
should give you a URL for your function when it’s finished. You can now add that URL to the test HTML file instead of localhost to test the live version.
Accept File Attachments
New let’s send some attached files with the form, note that most Email servers, and some Email clients, impose a size limit to Emails and attachments. Anything under 10MiB should be safe for most Email systems, if you need to transfer larger files it’s time to start looking at S3 storage or the like.
We’ll start by adding an NPM package to parse the data coming in from the form.
npm install lambda-multipart-parser
Now let’s set up a new message generator, I’ve highlighted the difference between this one and the first one we did. Since we’ll send this directly without putting JavaScript in between, we need to convert the field g-recaptcha-response
to recaptcha
here.
src/message-generator/multipart.js
const multipartParser = require('lambda-multipart-parser');
module.exports = async ({ event, emailFrom, emailTo, verifier }) => {
const {
firstName,
lastName,
email: userEmail,
message,
files,
'g-recaptcha-response': recaptcha,
} = await multipartParser.parse(event);
await verifier.verify({ response: recaptcha }, (err) => {
if (err) {
throw new Error('reCAPTCHA validation failed');
}
});
const messageBody = `
First Name: ${firstName}
Last Name: ${lastName}
Email: ${userEmail}
Message:
${message}
`;
const receiptBody = `
Hi ${firstName},
Thanks for reaching out, I'll be in touch soon!
`;
const email = {
to: emailTo,
from: emailFrom,
replyTo: `${firstName} ${lastName} <${userEmail}>`,
subject: 'Contact Form',
text: messageBody,
attachments: files.map(
({ filename, content, contentType, encoding }) => {
return {
filename,
content,
contentType,
encoding,
};
},
),
};
const receipt = {
to: userEmail,
from: emailFrom,
subject: 'Contact Form',
text: receiptBody,
};
return { email, receipt };
};
Now we export that from the index file.
src/message-generator/index.js
const googleRecaptcha = require('google-recaptcha');
const json = require('./json');
const multipart = require('./multipart');
const {
EMAIL_TO: emailTo,
EMAIL_FROM: emailFrom,
RECAPTCHA_SECRET,
} = process.env;
const verifier = new googleRecaptcha({ secret: RECAPTCHA_SECRET });
module.exports.json = async (event) => {
return await json({
event,
emailTo,
emailFrom,
verifier,
});
};
module.exports.multipart = async (event) => {
return await multipart({
event,
emailTo,
emailFrom,
verifier,
});
};
And export it from the main hanlder.js
file.
src/handler.js
require('dotenv').config();
const sendmail = require('./mailer/sendmail');
const messageGenerator = require('./message-generator');
const responseGenerator = require('./response-generator');
module.exports.json = async (event) => {
try {
const { email, receipt } = await messageGenerator.json(event);
const emailResult = await sendmail(email);
const receiptResult = await sendmail(receipt);
return !!emailResult && !!receiptResult
? responseGenerator.success(event)
: responseGenerator.error(event);
} catch (err) {
console.log(err);
return responseGenerator.error(event);
}
};
module.exports.multipart = async (event) => {
try {
const { email, receipt } = await messageGenerator.multipart(event);
const emailResult = await sendmail(email);
const receiptResult = await sendmail(receipt);
return !!emailResult && !!receiptResult
? responseGenerator.success(event)
: responseGenerator.error(event);
} catch (err) {
console.log(err);
return responseGenerator.error(event);
}
};
We’ll add it into the serverless.js
file as a second function, we also need to specify that the API Gateway should accept multipart/form-data
as a binary media type, this just allows the function to accept binary data from the form.
Serverless.yml
plugins:
- serverless-offline
service: form-handler-demo
provider:
name: aws
runtime: nodejs12.x
region: us-west-1
environment:
EMAIL_TO: REPLACE_THIS_WITH_EMAIL_ADDRESS
EMAIL_FROM: REPLACE_THIS_WITH_EMAIL_ADDRESS
MAIL_SERVER: REPLACE_THIS_WITH_EMAIL_SERVER
MAIL_PORT: REPLACE_THIS_WITH_EMAIL_SERVER_PORT
MAIL_USER: REPLACE_THIS_WITH_EMAIL_API_KEY_ID
MAIL_PASSWORD: REPLACE_THIS_WITH_EMAIL_API_KEY_SECRET
RECAPTCHA_SECRET: REPLACE_THIS_WITH_RECAPTCHA_SECRET
apiGateway:
binaryMediaTypes:
- 'multipart/form-data'
package:
exclude:
- .env
- .env.example
- .gitiginore
- package.json
- package-lock.json
- serverless.yml
- serverless.example.yml
functions:
json:
handler: src/handler.json
events:
- http:
path: /json
method: post
cors:
origin: '*'
headers:
- Content-Type
multipart:
handler: src/handler.multipart
events:
- http:
path: /multipart
method: post
cors:
origin: '*'
headers:
- Content-Type
Test with Attachments
Let’s run npm start
to spin up the local development instance first, you should see there are now two URL’s /dev/json
, and /dev/multipart
.
We’ll need to tweak the HTML page we used to test the json submission function, I’ve removed the script
tag entirely and highlighted the other changes here.
<!DOCTYPE html>
<html>
<head>
<script
src="https://www.google.com/recaptcha/api.js"
async
defer
></script>
<style>
form {
display: flex;
flex-direction: column;
width: 304px;
margin: 2rem auto;
}
input {
margin-bottom: 1rem;
}
button {
margin-top: 1rem;
}
</style>
</head>
<form
id="form"
method="post"
action="http://localhost:3000/dev/multipart"
enctype="multipart/form-data"
>
<label for="firstName">First Name</label>
<input
type="text"
name="firstName"
/>
<label for="lastName">Last Name</label>
<input
type="text"
name="lastName"
/>
<label for="email">Email Address</label>
<input
type="text"
name="email"
/>
<label for="message">message</label>
<input
type="text"
name="message"
/>
<input
type="file"
name="files"
multiple
/>
<div
id="recaptcha"
class="g-recaptcha"
data-sitekey="6LfZR9wUAAAAADZEFcYfXqpymjN-hwtsb5Aj2Csd"
></div>
<button type="submit">Submit</button>
</form>
</html>
As before, you can deploy to AWS using npm run deploy:dev
or npm run deploy:prod
, and again just change the target URL in the test HTML file to the one that serverless
spits out in the command line after deploying.
Conclusion
You have learned how to use AWS Lambda serverless functions to receive form submissions and send them out by Email, now you can add a sweet “Contact Me” form to your server-side rendered site with ease!