Skip to main content
How To Build A Serverless Contactform With Zeit
  1. Blog/

How To Build A Serverless Contactform With Zeit

·5 mins·
Table of Contents

Serverless platforms have been getting a lot of attention. AWS announced a ton of things at their annual user conference, Google announced support for Go in private beta and serverless containers in private alpha, and even Gitlab announced some form of serverless support. With all the big players, it’s easy to overlook the smaller ones — but they’re often the most interesting.

Zeit
#

One of those “smaller” platforms I came across was Zeit. Their mission is to “Make Cloud Computing as Easy and Accessible as Mobile Computing.” Underneath that, it reads: “We build products for developers and designers. And those who aspire to become one.” That sets a high bar for how their products should work, and I was curious to see if it held up. So I set out to build a simple function to serve as the backend for the contact form on stigterhq.nl.

Here’s a walkthrough of the code and a few lessons I learned building a Go app on Zeit. The app takes an HTTP request, validates the reCAPTCHA to make sure a bot didn’t fill out the form, and sends an email to a pre-determined address. The full code is available on GitHub.

The important files:

.
├── .env_template <-- A template file with the environment variables needed for the function
├── index.go      <-- The actual function code
└── now.json      <-- Deployment descriptor for Zeit

Zeit handles secrets similarly to regular environment variables, which keeps the code simple. The same os.Getenv() works for both secrets and regular env vars. One downside: you can’t update secrets in place. You have to delete and recreate them. In my case, the .env_template has the variables I need and a Makefile target handles the delete-and-recreate cycle.

Deployments (via the macOS app or CLI) rely on a now.json to tell the Zeit builders what to do with your code. It’s straightforward to set up and their docs are helpful. You can have multiple builds sections for a monorepo with frontend and backend code (usually not a great idea, but fine for experimenting). Environment variables are listed with their value directly or prefixed with @ to indicate they’re secrets.

The main file is index.go, which contains all the function logic. Here’s the breakdown:

// Constants
const (
  // The URL to validate reCAPTCHA
  recaptchaURL = "https://www.google.com/recaptcha/api/siteverify"
)

// Variables
var (
  // The reCAPTCHA Secret Token
  recaptchaSecret = os.Getenv("RECAPTCHA_SECRET")
  // The email address to send data to
  emailAddress = os.Getenv("EMAIL_ADDRESS")
  // The email password to use
  emailPassword = os.Getenv("EMAIL_PASSWORD")
  // The SMTP server
  smtpServer = os.Getenv("SMTP_SERVER")
  // The SMTP server port
  smtpPort = os.Getenv("SMTP_PORT")
)

This section reads in the environment variables. The code doesn’t care whether they’re secrets or regular env vars.

// Handler is the main entry point into tjhe function code as mandated by ZEIT
func Handler(w http.ResponseWriter, r *http.Request) {
  // HTTPS will do a PreFlight CORS using the OPTIONS method.
  // To complete that a special response should be sent
  if r.Method == http.MethodOptions {
    response(w, true, "", r.Method)
    return
  }

  // Parse the request body to a map
  buf := new(bytes.Buffer)
  buf.ReadFrom(r.Body)
  u, err := url.ParseQuery(buf.String())
  if err != nil {
    response(w, false, fmt.Sprintf("There was an error sending your form data: %s", err.Error()), r.Method)
    return
  }

  // Prepare the POST parameters
  urlData := url.Values{}
  urlData.Set("secret", recaptchaSecret)
  urlData.Set("response", u["g-recaptcha-response"][0])

  // Validate the reCAPTCHA
  resp, err := httpcall(recaptchaURL, "POST", "application/x-www-form-urlencoded", urlData.Encode(), nil)
  if err != nil {
    response(w, false, fmt.Sprintf("There was an error sending your form data: %s", err.Error()), r.Method)
    return
  }

  // Validate if the reCAPTCHA was successful
  if !resp.Body["success"].(bool) {
    response(w, false, fmt.Sprintf("There was an error sending your form data: %s", fmt.Sprintf("%v", resp.Body["error-codes"])), r.Method)
    return
  }

  // Set up email authentication information.
  auth := smtp.PlainAuth(
    "",
    emailAddress,
    emailPassword,
    smtpServer,
  )

  // Prepare the email
  mime := "MIME-version: 1.0;\nContent-Type: text/plain; charset=\"UTF-8\";\n\n"
  subject := fmt.Sprintf("Subject: [BLOG] Message from %s %s!\n", u["name"][0], u["surname"][0])
  msg := []byte(fmt.Sprintf("%s%s\n%s\n\n%s", subject, mime, u["message"][0], u["email"][0]))

  // Connect to the server, authenticate, set the sender and recipient,
  // and send the email all in one step.
  err = smtp.SendMail(
    fmt.Sprintf("%s:%s", smtpServer, smtpPort),
    auth,
    emailAddress,
    []string{emailAddress},
    msg,
  )
  if err != nil {
    fmt.Printf("[BLOG] Message from %s %s\n%s\n%s\nThe message was not sent: %s", u["name"][0], u["surname"][0], u["message"][0], u["email"][0], err.Error())
    response(w, false, "There was an error sending your email, but we've logged the data...", r.Method)
    return
  }

  // Return okay response
  response(w, true, "Thank you for your email! I'll contact you soon.", r.Method)
  return
}

The entry point function is called Handler — that’s a Zeit requirement you can’t change.

There are two helper methods in the code:

  • response: handles sending replies to the incoming request. Since most replies follow the same pattern, a single method made sense.
  • httpcall: calls the reCAPTCHA service.

Why is everything in one file? The Zeit Go builder treats every file as a separate build artifact, so splitting things into http.go and main.go wasn’t an option. I also found that the builder looks for the first “exported” method and ignores the rest. I could work around that by moving Handler to the top, but a better fix would be for the builder to check whether a function is exported and has the right signature.

Conclusion
#

Once I figured out the guardrails, the serverless contact form worked perfectly. And honestly, those guardrails aren’t bad. With a pretty generous free plan, a solid set of runtimes (PHP, Next.js, even Markdown), ease of use, and some of the things the team tweeted about, Zeit has a pretty interesting time ahead (yes, I did totally want to make a time related pun). I hope they’ll continue their service for a long time, not in the very least because they’re awesome contributors to Open Source.

Cover image by Pixabay

Related

Deploying Flogo Apps to Lambda with the Serverless Framework (Part 2)

·4 mins
I can hear you think “Part 2?! So there actually is a part 1?” 😱 The answer to that is, yes, there most definitely is a part 1 (but you can safely ignore that 😅). In that part I went over deploying Flogo apps built with the Flogo Web UI using the Serverless Framework. Now, with the Go API that we added to Flogo, you can mix triggers and activities from Flogo (and the community) with your regular Go code and deploy using the Serverless Framework.

How To Build a Slack Bot Powered By Project Flogo

·4 mins
This post walks through building a Slack bot that responds to a /cat slash command with cat facts. The bot is built with Project Flogo, runs on AWS Lambda, and is exposed through API Gateway. The whole thing takes about 15 minutes to set up.