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 ZeitZeit 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