Form handling using Azure functions

Written by Dariusz Sobczyk on 21 July 2019 ~ 16 min read

In this article, I’m going to explain how to build a custom form handler using Azure functions to send e-mail notifications using SendGrid.

When you control back-end code for websites that you build, handling form submissions is straightforward. You write some code on the server to process form data, send an email notification and you’re done. However, writing custom server code is not always possible. For example, if you host a website in a service like Azure Storage or GitHub Pages, you don’t have access to the back-end. Therefore, you cannot write any custom server code to handle forms on your website. If you already use Azure, you can solve this problem relatively easy by building a custom Function App, and setting up a SendGrid account for email notifications. Let’s go step-by-step to see how to do it.

Prerequisites

I assume that you have an Azure account set up and a subscription plan that allows you to create and manage resources. If you don’t have an Azure account, consider creating an account for free.

We’re going to perform most of the steps from the terminal, so you should have Azure CLI installed on your system. If you are not familiar with Azure CLI, take a look at the official documentation. Before you go further, make sure that you are logged in to Azure using the az login command, and that you have the proper subscription selected.

Creating a resource group

The very first thing we are going to do is to create a new resource group for this tutorial. For that, use the az group create command, with additional arguments to create a new resource group:

az group create --l westeurope --n demo-form-rg

The above command creates a new resource group named demo-form-rg in the West Europe region. When you no longer need it, you can delete the resource group and all the associated resources with az group delete command, like so:

az group delete -n demo-form-rg

Feel free to change the location of the resource group. If you decide to do so, you will want to keep the same location for every resource created in the following steps.

Creating a storage account

A storage account is required to store the source code, logs, and other data related to your function app. You could use an existing storage account, but it’s a good practice to create a new one to keep things nicely separated and organized. Use the az storage account create command with additional arguments to create a new storage account:

az storage account create -n demoformstorage -g demo-form-rg -l westeurope --sku Standard_LRS

The above command creates a new storage account named demoformstorage inside the demo-form-rg resource group. The location of the storage account is West Europe, which is the same as the location of the resource group. The --sku argument sets the storage redundancy (SKU). It tells Azure how to replicate your data across data centers and regions. You can learn more about storage redundancy options from the official documentation. For this tutorial, I’m using Standard_LRS which stands for locally redundant storage. It is a low-cost solution that guarantees good durability by replicating the data within a single datacenter.

Creating a function app

The next step is to create a function app resource. It will enable us to store and manage the source code and configuration of our functions. Use the az functionapp create command with additional arguments to create a new function app:

az functionapp create -g demo-form-rg -s demoformstorage -n demo-form-app -c westeurope

The above command creates a function app named demo-form-app inside the demo-form-rg resource group. The location of the function app is again, West Europe, which is the same as the location of the storage and the resource group. You can learn more about Azure Functions by diving into the official documentation.

Writing function code

We start implementing our function by writing a simple HTTP request handler to verify whether the function executes correctly. Create a new directory named demo-form-app that will contain the source code and configuration of our function app:

mkdir demo-form-app && cd demo-form-app

Azure requires that the function app source code is organized into subdirectories containing individual function implementations (a function app consists of multiple functions). Create a subdirectory named demo-form-fn, which will contain the implementation of our form handler:

mkdir demo-form-fn && cd demo-form-fn

Inside the demo-form-fn, create a new file named index.js and paste the following code:

module.exports = async function (context, req) {
  context.res = {
    status: 200,
  }
}

This is a simple handler that for every request responds with a HTTP 200 OK code. Now, still in the demo-form-fn directory, create a file named function.json and paste the following configuration:

{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

In the function.json file, we store the configuration of our function. We set the function type to the HTTP trigger, and the authentication level to function, which will require us to specify a unique code in the function URL in order to execute it. The method that will trigger execution of our function is set to post. You can learn more about other options from the official documentation

Deploying the function

The next step is to deploy the function to Azure. The easiest way is to use the ZIP deployment feature. First, use zip command to package the source code and the configuration of the function together:

zip -r demo-form-app.zip *

Next, use the az functionapp deployment command with additional arguments to deploy the function. Note that you have to package files from inside the demo-form-app directory:

az functionapp deployment source config-zip -g demo-form-rg -n demo-form-app --src demo-form-app.zip

Testing the function

After the deployment is complete, we can test whether our function executes correctly. Before we make the first request however, we need to obtain the function’s URL. Unfortunately, it cannot be done using Azure CLI, so we need to go to the Azure Portal. Find the demo-form-fn function in the resources, and open it. Click on </> Get function URL, and copy the displayed URL to the clipboard.

Getting URL of the function

Note that the URL contains a code parameter that I mentioned earlier. We can now make our very first request to check whether the function works correctly. Open up the terminal and use the curl command with additional arguments to make the request (replace $url with the URL of your function):

curl -X POST -d '' -i $url

The above command will send an HTTP request to the specified URL and display the response. The -X POST specifies that the request is a POST request. The -d '' with empty parens sets the content to empty, and adds a content-length with value 0 to the request headers. Without it, our request would never trigger the function. You can check that by removing -d '' entirely. The -i switch informs curl to print out the response header. If your output is similar to the one below, it means that the function has executed correctly:

HTTP/2 200 
content-type: text/plain; charset=utf-8
date: Sun, 20 Jul 2019 07:18:00 GMT
content-length: 0

Now that we know that our function can respond to requests, we can write a form to submit some data.

Implementing a form

The form we are going to build is a simple contact form. It will contain two text fields for providing an email address and a message. The form submission process will be handled purely in JS instead of letting the browser to post data for us. The reason behind this is that we want to avoid page reloads when submitting a form, and have greater control over the format of the data. In order to reduce the noise, the form implementation doesn’t contain any validation, extended error handling, or styling. Create an index.html file anywhere on your disk, and paste the following code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Form submission using Azure Function</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <form id="contactForm">
      <label for="email">Your e-mail:</label>
      <input id="email" name="email">
      <input id="message" name="message">
      <input type="submit" value="Submit">
    </form>

    <script>
      // Find the form in the document.
      var form = document.querySelector('#contactForm')

      // Listen for the submit event.
      form.addEventListener('submit', function(event) {
        // Prevent default behavior which is form submission and refresh.
        event.preventDefault()
        // Submit form data.
        submit()
      })

      function submit() {
        // Holds a function URL with a code argument.
        var FUNC_URL = ''

        // Create a FormData object that will contain easily accessible form data.
        var data = new FormData(form)

        // Create a new request object.
        var xhr = new XMLHttpRequest()

        // Listen for successful form submission event.
        xhr.addEventListener('load', function(event) {
          alert('Form has been submitted!')
        })

        // Listen for failed form submission event.
        xhr.addEventListener('error', function(event) {
          alert('Oops! Something went wrong.')
        })

        // Set the request type to POST.
        xhr.open('POST', FUNC_URL)

        // Set the content type of our request as JSON.
        xhr.setRequestHeader('Content-Type', 'application/json')

        // Send the request containing email and message fields.
        xhr.send(JSON.stringify({
          email: data.get('email'),
          message: data.get('message'),
        }))
      }
    </script>
  </body>
</html>

Our form handler will read the contents of the request, and if it contains email and message fields, it will notify us by sending an email. Before we are able to implement that, we need to set up a SendGrid account, and get an API key.

Creating SendGrid account

SendGrid accounts can be created within the Azure Portal. This way, you can send 25k free emails each month without additional costs. From the Azure Portal, go to “Create a resource”, and in the search box type “SendGrid”, and press enter.

Setting up SendGrid account

On the next screen click “Create”, and follow the instructions.

Setting up SendGrid account

After you fill in all the required information, click “Create”. It can take a few minutes for the account to be ready. Find the account on the list of your resources, and open it. Click “Manage”, and you will be redirected to a new page where you can set up your API keys.

Managing SendGrid account

The first time you visit the page, you will be asked to confirm your account by clicking on the activation link you received in an email.

Creating an API key

In the SendGrid management panel, go to “Settings”, then “API Keys”, and click on “Create API Key” in the top right corner of the page.

Creating SendGrid API Key

Enter the API key name, then select “Restricted Access”, and in the “Mail Send” section select “Mail Send”. Click “Create and View”.

Creating SendGrid API Key

Copy the key from the new window, and store it somewhere because you will not be able to access it later in any way. Having an API key ready, we can now implement the form handler to send e-mail notifications right after receiving form data.

Handling form submission

Before we write code to send e-mail notifications, we have to install official SendGrid SDK for Node.js. Go the the demo-form-app/demo-form-fn directory, and init the project using npm:

npm init -y

Install the SDK using npm:

npm install --save @sendgrid/mail

Now, replace the index.js file contents with the following code, and save it to the disk:

// Import official SendGrid SDK.
const sendgrid = require('@sendgrid/mail')

// Holds your SendGrid API key.
const API_KEY = ''

// Holds an e-mail address to which notifications will be delivered.
const EMAIL = ''

module.exports = async function (context, req) {
  if (!req.body.email || !req.body.message) {
    // Respond with 422 Unprocessable Entity if the body doesn't contain both email and message fields.
    context.res = {
      status: 422,
    }
  }
  else {
    // Set API key to use for sending e-mails.
    sendgrid.setApiKey(API_KEY)

    // Send an e-mail.
    sendgrid.send({
      to: EMAIL,
      from: req.body.email,
      subject: 'Contact request',
      text: req.body.message,
    })
  }
}

You can now deploy the function again in the same way as we did previously. Package it with zip command, and use the az functionapp deployment command to deploy it to Azure.

Testing form submission

Open the index.html file containing the form code in a browser. Fill in the email and message fields, and click the send button. After a while, you should see a message box saying that something went wrong. Don’t worry, this is expected. The reason behind the error is that browsers don’t allow to post data to a different origin by default. The mechanism of this behavior is called “Cross-Origin Resource Sharing”, or CORS for short. The origin in this case is your file system, and you attempted to post data to a different URL, i.e. the URL of your function app. You can learn more about CORS by reading this excellent article on MDN.

There are two ways to fix this. The first way is to use Chrome (or Chromium) browser and disable web security checks by starting it with --disable-web-security --user-data-dir="/tmp/chrome_tmp" arguments. This is useful for debugging purposes and local development, but if you decide to publish your form, you will have to adjust the configuration of you function app.

Configuring CORS

Open the Azure Portal, and navigate to your function app settings. Find the CORS settings and open it.

CORS settings

In the “Allowed Origins” section, add the full addresses of domains from which your contact form handler can receive requests. You should add both HTTP and HTTPS if you are using them. For example, if the HTML document that contains the form is located at https://www.example.com, and your form handler is at https://www.example-form.com, then you should add https://www.example.com to the CORS configuration of your function.

CORS settings

You can test the form again after dealing with the CORS settings. This time you should see a message telling you that the form has been submitted. Make sure to enter a valid email address, or SendGrid won’t process your request.

Summary

The form that we’ve built is far from production ready. There are a lot of things that can be improved. For example, if you build a lot of websites that contain similar forms, you could handle all submissions with a single function. If you need the separation, you could implement a single form handler, store it in a Git repository, and use the git deployment feature. In this case, the settings like a destination email address, and SendGrid API key wouldn’t be stored in the source code. Instead, you could use the function app settings to store and access them from the source code. The implementation should also contain form validation logic. I recommend doing it in both front-end and back-end implementation for better security and user experience.

You can find the source code for this tutorial on GitHub. Remember to change your function app URL, SendGrid API key, and e-mail address before deploying.