Building a Form Handler Service in OpenWhisk

As a fan of static site generators, I've played with, and built, my own form processing services to work with my static sites. My current favorite is FormSpree - it's the one I use on this blog. I even built my own version of it in Node ("Building a Simple Form Handler Service in Node") back in October last year. I thought it would be kind of cool to look into building a similar type service with OpenWhisk.

Right now, OpenWhisk isn't necessarily the best choice for this type of service. The main reason being that you can't return HTTP headers in your response. That means I can't take a form and simply set the action to an OpenWhisk URL. Hopefully that will come in the future. But I can use Ajax to process the form and that's the route I've taken for this demo.

The basic premise is this:

  • Accept a set of form data
  • Email that form data
  • Look for special keys possibly change that email, like _subject, and _from.

For sending email, I decided to use the sendmail NPM package which is supported out of the box with OpenWhisk. nodemailer is also supported, but I wanted to try something new. After creating my API key, I wrote up the following action.


//SendGrid API Key
var SG_KEY = 'noonecommentsonthefunnythingsiputinmykeys';

//people we are allowed to email
var RECIPS = ["raymondcamden@gmail.com","ray@camdenfamily.com"];

var helper = require('sendgrid').mail;

function main(args) {

    let from_email = new helper.Email(RECIPS[0]);
    let to_email = new helper.Email(RECIPS[0]);

    /*
    Ok, so args will be our form data. Look for a few special item
    */

    if(args["_from"]) {
        from_email = new helper.Email(args["_from"]);
    }

    if(args["_to"]) {
        if(RECIPS.indexOf(args["_to"]) === -1) {
            return {error:"Invalid _to address: "+args["_to"]};
        } else {
            to_email = new helper.Email(args["_to"]);
        }
    }

    let subject = 'Form Submission';

    if(args["_subject"]) {
        subject = args["_subject"];
    }

    /*
    Now loop through the rest of the args to create the form submission email.
    */

    //todo: make date a bit prettier
    let date = new Date();
    let content = `
Form Submitted at ${date}
--------------------------------
`;

    for(let key in args) {
        //blanket ignore if _*
        if(key.indexOf("_") != 0) {
            content += `
${key}:         ${args[key]}
`;
        }
    }

    let mailContent = new helper.Content('text/plain', content);
    let mail = new helper.Mail(from_email, subject, to_email, mailContent);
    let sg = require('sendgrid')(SG_KEY);

    return new Promise( (resolve, reject) => {

        var request = sg.emptyRequest({
            method: 'POST',
            path: '/v3/mail/send',
            body: mail.toJSON()
        });
        
        sg.API(request, function(error, response) {
            if(error) {
                console.log(error.response.body);
                reject({error:error.message}) 
            } else {
                console.log(response.statusCode);
                console.log(response.body);
                console.log(response.headers);
                resolve({result:'Ok'});
            }
        });

    });

}

exports.main = main;

Let's tackle this line by line. I assume you can figure out what SG_KEY is. The variable, RECIPS, is used to create a whitelist of emails that can be the recipient. This is unlike FormSpree which will email anyone (with confirmation of course!) but I figured this was a more realistic example. I used an array since I thought for a real "dot com" there may be a few different forms in use.

Going into main, you can see how I look at my arguments for possible overrides of behavior. You can override the from address, the to (again though, only within the acceptable list), and subject. I then craft a string of the remaining values and send that to SendGrid. And that's it. I could obviously make this a bit nicer, switch to HTML email too perhaps, but you get the idea.

I then simply used the OpenWhisk command line to create the action and API end point. (Although before I did that, I tested locally.)

Let's look at the front end. First, I built a really ugly form.


<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">
    </head>
    <body>

        <h2>Contact Form</h2>

        <form id="contactForm">
        <p>
        <label for="name">Name</label><br/>
        <input id="name" type="text" required>
        </p>

        <p>
        <label for="email">Email</label><br/>
        <input id="email" type="email" required>
        </p>

        <p>
        <label for="comments">Comments</label><br/>
        <textarea id="comments"></textarea>
        </p>

        <p>
            <input type="submit" value="Send My Important Comments">
        </p>
        </form>

        <script src="app.js"></script>
    </body>
</html>

And then wired up the code.


let $name, $email, $comments, $contactForm;

const API_URL = 'https://3b1fd5b1-e8cc-4871-a7d8-cc599e3ef852-gws.api-gw.mybluemix.net/api/sendmail';
                
document.addEventListener('DOMContentLoaded', init, false);

function init() {
    $name = document.querySelector('#name');
    $email = document.querySelector('#email');
    $comments = document.querySelector('#comments');
    $contactForm = document.querySelector('#contactForm');

    $contactForm.addEventListener('submit', processForm, false);
}

function processForm(e) {
    console.log('processForm ftw');
    e.preventDefault();

    /*
    create a structure of form data to send to the api
    */
    let data = {
        name:$name.value,
        _from:$email.value,
        _subject:"My Cool Contact Form",
        comments:$comments.value
    };

    fetch(API_URL, {
        method:'POST', 
        mode:'cors',
        body:JSON.stringify(data)
    }).then((res) => {
        return res.json();
    }).then( (result) => {
        console.log('result was '+JSON.stringify(result));    
        if(result.result === 'Ok') {
            $comments.value='';
            alert("Thanks for the awesome comments!");
        } else {
            alert("Sorry, something went wrong.");
        }
    });
}

In the end, my code is simple DOM reading and Ajax-fetching. You do have to be careful about how you POST to OpenWhisk. You cannot send a regular form body and must instead send a JSON packet of data. But frankly that wasn't necessarily a big deal. And of course - it works!

Yes - the formatting is a bit subpar, but for a quick demo, I'll leave it be for now. So what do you think?

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

Want to read more like this?