Static websites are fast, cheap and secure. But how do you add a server side function like a contact form? This blog post explains, how you can implement a serverless contact form by using AWS Lambda and the serverless framework.

As you may have noticed from my previous blog post, this blog is a Jamstack site. That means this blog is completely static and served with a static web hoster.

What does static mean?

Static does not mean, the content is not changing or updating dynamically. It just means the website does not need to be rendered on a backend server.

Today we have different solutions for serving websites or web apps:

Traditional web servers

Traditionally you would render a website on a server for example with PHP Laravel, Java Springboot.

Meaning everytime you access a webpage, a html document is generated on the server and served to the client.

This is great for SEO, as search engines do not need to process Javascript to understand the webpage.

The drawback is, you need a full backend with a server that is running constantly.

SPA - Single Page Applications

Single page applications can be served with a static webserver; a server that does nothing but serving assets like HTML, CSS and Javascript without generating anything on the server side. The HTML is then rendered on the client side; in the web browser, by using Javascript.

This is great, because we the webpage itself can change by interacting with it, without reloading the page. We can also cache it and make it available offline. (See How to create a PWA)

The drawback is, this is not easy to crawl by search engines, because they now need to run Javascript in order to retrieve the content.

SSR - Server Side Rendering

Then there’s SSR. SSR combines the last two points, by generating parts of your Single Page Application on the server side, without sacrificing on dynamic client side content, offline caching, etc.

Some Javascript framworks that include SSR are: Sapper, Next.js, Nuxt.js

This is a lot better for SEO, as content is not rendered beforehand and served as HTML.

Now still, you now need a Node.js or similar server with a SSR framework.

Static generated websites - JAMstack

Now finally, we have static generated websites.

Most SSR frameworks not only allow us to dynamically render a webpage on a server, but also to prerender it before uploading it to you webhoster!

This means we have all the benefits of a Server Side Rendered webpage, but we can deliver it completely statically on any hosting solutions. This also means we can deploy it to global CDNs, which are cheap and fast. You can host this for example on Github Pages, Amazon S3, Zeit, or Netlify.

This is also called JAMstack.

JAMstacks have following benefits:

  • it does not rely on any specific server, but can be hosted on any static hoster
  • it is as fast as it can get, because there is not server rendering
  • it is cheap to host
  • it is more secure and reliable, as your main webpage is decoupled from your backend services (see below)

Now this is awesome, as long as you don’t have a function on you web app, that requires a server side script. Like a contact form, authentication or anything you need a database for.

This is were serverless services come into play:

Serverless

Serverless does of course not mean we don’t need a server. Instead we call a service serverless, if we do not need to have a server running for ourself.

Instead of managing our own webserver, we create microservices for different functions we required in our app. These services or functions then only run, when required, so you only pay per request.

This is especially useful for contact forms, where you don’t have lots of request.

Let’s create a serverless contact form

Now let’s set up a contact form for a static website as an example.

Requirements

In this guide, we use Amazon AWS with AWS Lambda to create serverless functions. So to follow along, a AWS Account is required.

AWS has a free tier, so this contact form works basically for free.

Configure simple email service

Amazon AWS provides a simple email service for sending and receiving emails. So first we need to set this up by going into the aws console and seach for “Simple Email Service”.

screenshow

Click on email adresses and “Verify a New Email Adress”. Enter your email and verify it using the link sent to your email account.

Add a IAM User for authenticating

Next create a IAM User as documented here: https://serverless.com/framework/docs/providers/aws/guide/credentials#creating-aws-access-keys

TL;DR:

  1. Search for IAM in your AWS Console
  2. Click on Users then Add user
  3. enter a username
  4. check programmatic access
  5. click next
  6. “Attach existing policies directly”, search for AdministratorAccess
  7. Next: Review -> Create User
  8. Copy your API Key and API Secret somewhere temporary

This grants complete administrator permissions for this user. You might want to change these policies when using in production.

Serverless framework

The serverless frameworks makes it easy to create and deploy one or more serverless functions to AWS connecting different AWS services.

Install serverless: npm i -D serverless

To automatically set up and deploy your services, serverless needs to access your account by using the IAM User and authentication keys from the previous step:

sls config credentials \
    -p aws \
    -k YOURACCESS_ID \
    -s YOUR_SECRET

sls is just a shortcut for serverless

Use the Access ID and Secret from your created IAM user before.

Now we can create a new serverless function. In this post, we use Node.js, so we just use the aws-nodejs template.

sls create --template aws-nodejs --path your-service-name

This creates a folder named your-service-name.

Open it and edit serverless.yml in your favourite editor. See:https://docs.serverless.com

Example configuration:

service: your-service-name

custom:
  env: ${file(.env.json)}

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  region: eu-central-1
  environment:
    NODE_ENV: ${self:custom.env.NODE_ENV}
    EMAIL: ${self:custom.env.EMAIL}
    DOMAIN: ${self:custom.env.DOMAIN}
  iamRoleStatements:
    - Effect: 'Allow'
      Action:
        - 'ses:SendEmail'
      Resource: '*'

functions:
  send:
    handler: handler.send
    events:
      - http:
          path: email/send
          method: post
          cors: true

With the http event, serverless automatically creates an api gateway endpoint on aws. So we don’t need to set this up manually.

As you can see in the example above, I used a .env.json file to store configuration in a separat file. This makes sure, that you don’t accidentally push your secrets to git.

So, create a .env.json file for you enviroment variables and add it to your .gitignore.

.env.json

{
  "NODE_ENV": "dev",
  "EMAIL": "EMAIL_ADRESS_TO_SEND_TO",
  "DOMAIN": "*"
}

Make sure you add your domain instead of * when using in production. (for cors headers)

Now we need to create our function to send emails. For that edit the handler.js file.

You find my file as an example below:

handler.js

'use strict';

const aws = require('aws-sdk');
const ses = new aws.SES();
const sendToEmail = process.env.EMAIL;
const domain = process.env.DOMAIN;

const response = (code, body) => {
  return {
    statusCode: code,
    headers: {
      'Access-Control-Allow-Origin': domain,
      'Access-Control-Allow-Headers': 'x-requested-with',
      'Access-Control-Allow-Credentials': true
    },
    body: JSON.stringify(body || {})
  };
};

const emailTemplate = ({ email, name, content }) => {
  return {
    Source: sendToEmail,
    Destination: { ToAddresses: [sendToEmail] },
    ReplyToAddresses: [email],
    Message: {
      Body: {
        Text: {
          Charset: 'UTF-8',
          Data: `Message sent from email ${email} by ${name} \nContent: ${content}`
        }
      },
      Subject: {
        Charset: 'UTF-8',
        Data: `You received a message from ${domain}!`
      }
    }
  };
};

module.exports.send = async event => {
  try {
    let { email, subject, name, content } = JSON.parse(event.body);
    // validation checks
    if (!(email && name && content)) {
      throw new Error(
        "Missing parameters! Make sure to add parameters 'email', 'name', 'content'."
      );
    }
    // VERY basic validation
    if (!email.match(/@/)) {
      throw new Error('Wrong email format.');
    }

    // honeypot
    if (subject) {
      throw new Error('The submission was recognized as spam.');
    }

    await ses.sendEmail(emailTemplate({ email, name, content })).promise();

    return response(200, { message: 'Message sent.' });
  } catch (e) {
    console.log(e);
    return response(500, { message: e.message });
  }
};

We required the AWS provided SDK to get the Simple Email Service and send our mails on request. This function does not need any dependencies!

I also added a honeypot field to prevent bots from spamming.

You might also want to add a more advanced form validation here. This might be important, as we get user created input.

That’s it. Now we can deploy it with:

sls deploy

Note down you API endpoint, that is display in your console after deploying.

Client

For the client I added a simple html form with a honeypot field (named “subject”).

<form id="contact-form">
  <input type="text" placeholder="Name" name="name" required />
  <input type="email" placeholder="Email" name="email" required />
  <input type="text" name="subject" style="display: none;" />
  <textarea
    rows="4"
    placeholder="Your message"
    name="content"
    required
  ></textarea>
  <button id="submit-button" type="submit">Submit</button>
  <p id="submit-message"></p>
</form>

I wont bother with styling in this guide.

Now we need to retrieve the form data and send it as json to your api url from before:

const form = document.getElementById('contact-form');
form.addEventListener('submit', event => {
  event.preventDefault();
  const url = 'YOUR_API_URL';
  const submitButton = document.getElementById('submit-button');
  const submitMessage = document.getElementById('submit-message');

  submitButton.disabled = true;
  submitMessage.innerHTML = 'Sending...';

  // retrieve json from form
  const payload = [].reduce.call(
    form.elements,
    (data, element) => {
      if (element.name) data[element.name] = element.value;
      return data;
    },
    {}
  );

  const req = new XMLHttpRequest();
  req.open('POST', url, true);
  req.setRequestHeader('Content-Type', 'application/json');
  req.addEventListener('load', () => {
    if (req.status === 200) {
      submitMessage.innerHTML = 'Thank you for your message.';
      form.reset();
    } else {
      submitMessage.innerHTML =
        'Something went wrong sending the message. Please try again later! Thank you.';
      console.log(JSON.parse(req.responseText));
    }
  });
  req.send(JSON.stringify(payload));
});

There you have it. We just created a serverless contact form!