Build a serverless service for "Contact Me" form in Golang and deploy on GCP
Tags: go, serverless, googlecloudplatform
Published: 2020-01-09 12:24:54

"Contact me" forms are pretty common. You can use it for your portfolio website, contact section of your online business and so on.

This tutorial assumes our "Contact me" form has three fields: Name, Email, and Message. On hitting submit it will call a REST API which will trigger our GCP (Google Cloud Platform) function which in turn will forward above mentioned details to our email.

This tutorial will cover the following things:

 - Setting up the code with "[Serverless Framework](https://serverless.com)" in Golang.
 - Configuring Twilio SendGrid API.
 - Prepare GCP resources.
 - Deploying the Golang code on Google Cloud Functions.

#Setting up the code

We are using the "[Serverless Framework](https://serverless.com)" to manage our function. In this tutorial, we will configure it to support GCP. But it's very easy to change the provider to other platforms like AWS, Azure, etc.

If you don't have "Serverless Framework" installed on your machine, let's get it first by following below link (just install it, no need to login):

https://serverless.com/framework/docs/getting-started/


Create the codebase:
```
$ cd to/desired/location/in/GOPATH
$ serverless create --template google-go --name contactmebackend --path contactmebackend
```
Next, install the plugin required for GCP (I'm using yarn, feel free to use NPM):
```
yarn add serverless-google-cloudfunctions
```

You will get the following files under `contactmebackend` the directory:
```
Makefile
fn.go
fn_test.go
node_modules
package.json
serverless.yml
yarn.lock
```

We will use Twilio's SendGrid API to send the information coming from REST API to our email address. SendGrid has a Golang package to interact with its API. Since we will be deploying this code to GCP functions later, we will need to setup [Golang module](https://github.com/golang/go/wiki/Modules) so that GCP can figure out the external dependencies. Follow these steps to do just that:
```
$ export GO111MODULE=on
$ go mod init
$ go get -u github.com/sendgrid/sendgrid-go
```

Edit `fn.go` with the following code. I have provided comments to better understand the code (don't forget to replace it with your name and email id):
```golang
package contactmebackend
import (
"encoding/json"
"log"
"net/http"
"os"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
)
// Email struct defines the data coming via REST API
type email struct {
EmailAddress string `json:"emailAddress"`
Name         string `json:"name"`
Message      string `json:"message"`
}
// SendEmail function sents email with the data coming from REST API
func SendEmail(w http.ResponseWriter, r *http.Request) {
var e email
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&e)
if err != nil {
panic(err)
}
// Prepare the email content
from := mail.NewEmail(e.Name, e.EmailAddress)
subject := "Contacted via portfolio website"
to := mail.NewEmail("Your name", "Your email id")
plainTextContent := e.Message
message := mail.NewSingleEmail(from, subject, to, plainTextContent, plainTextContent)
// Get the SendGrid API key from environment variable
sendGridAPIKey := os.Getenv("SENDGRID_API_KEY")
// Generate a SendGrid Send Client
client := sendgrid.NewSendClient(sendGridAPIKey)
// Send the message
response, err := client.Send(message)
if response.StatusCode != http.StatusAccepted {
if response.StatusCode != http.StatusOK {
// Request failed
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Something bad happened!"))
}
} else {
// Request successful
// Setting header to allow cors
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte("Success"))
}
}
```

#Configuring Twilio SendGrid API

The only thing remaining for the above code to work as expected is the [SendGrid](https://sendgrid.com) API key. To get the key, [log in to SendGrid Console](https://app.sendgrid.com/login) first. Then go to [create API key](https://app.sendgrid.com/settings/api_keys) page. Click on Create API Key.

![Create API Key page](https://thepracticaldev.s3.amazonaws.com/i/3hl6h4pru7bv8a9zmwrt.png)

Enter the name for the key. You can keep the Full Access for API Key permissions and hit "Create & View". Copy the API key displayed on the next page and keep it with you in some secure place.

![API Key details page](https://thepracticaldev.s3.amazonaws.com/i/v2ylcpu5mupsqbcaxi63.png)

#Testing the code

To make sure the above code works, we will edit `fn_test.go` and run tests. Edit `fn_test.go` with the following code:
```golang
package contactmebackend
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestSendEmail(t *testing.T) {
// Mock data with the 'body' which will go over the REST, and 'wantStatus' which is the expected status for the data sent in 'body' field
tests := []struct {
body       string
wantStatus int
}{
// This one has all the necessary fields for successful request
{body: `{"name": "Itachi Redhair", "emailAddress": "itachi.redhair@gmail.com", "message": "Would love to meet over a beer!"}`, wantStatus: http.StatusOK},
// This one will fail the request because emailAddress is missing.
{body: `{"name": "Itachi Redhair", "message": "Would love to meet over a beer!"}`, wantStatus: http.StatusInternalServerError},
}
for _, test := range tests {
// httptest.NewRequest package will form the mock request object which we can pass on to our function directly
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(test.body))
req.Header.Add("Content-Type", "application/json")
// mock response object
rr := httptest.NewRecorder()
SendEmail(rr, req)
if got := rr.Result().StatusCode; got != test.wantStatus {
t.Errorf("SendEmail(%q) = %q, want %q", test.body, got, test.wantStatus)
}
}
}
```

Now before you run the tests, you need to set the environment variables. Run the following command with the SendGrid API key:
```
export SENDGRID_API_KEY={Paste the api key which you copied earlier}
```

Run the test with the following command:
```
go test
```
You should see output which should be somewhat like this:
```
2020/01/08 11:18:29 <nil>
PASS
ok      medium-code-dump/contactmebackend       1.003s
```

#Preparing GCP resources

Now that our code is ready, we need to deploy it. To deploy it on Cloud Functions we need to enable few GCP APIs. You can do so by following this [guide on serverless.com](https://serverless.com/framework/docs/providers/google/guide/credentials/).

Once you've enabled the required APIs, got credentials JSON file, stored it on your local machine and updated the `serverless.yml` file, edit `functions` field in `serverless.yml` field like below:
```yaml
functions:
  first:
    handler: SendEmail
    events:
      - http: dummy-text
```

`SendEmail` is the name of the handler function from our code in `fn.go`. Name of the function `first` doesn't really mean anything and is ignored, same goes for value of the field `http`. You can see this link for details.

#Deploying

We are just there. To deploy the code just hit the following command:
```
serverless deploy
```

Wait till the deployment is done. You will get the link to trigger the function at the end. Something like this:
```
Deployed functions
first
  https://us-central1-{project-id}.cloudfunctions.net/SendEmail
```

Before you try the above link, you need to specify the environment variable `SENDGRID_API_KEY` in your cloud functions.

Go to [Cloud Functions List](https://console.cloud.google.com/functions/list) in GCP console. Open our newly deployed function `SendEmail`. Click on edit. Scroll down and you will see an option to edit the environment variables. Add `SENDGRID_API_KEY` and it's value and hit deploy.

After your function is deployed, try the following command with the above link:
```
curl -X POST \
https://us-central1-{project-id}.cloudfunctions.net/SendEmail \
-H 'Content-Type: application/json' \
-d '{
"emailAddress":"dummy-email@dummy.com","name":"dummy name", "message":"Hi there would like to meet you!"
}'
```

Check your email and if everything goes fine you won't be disappointed.

#Wrapping up

This is a very simple and minimal example of how you can leverage Google Cloud Functions to deploy serverless service written in Golang.
I am using this for the contact me form I have on my portfolio and wanted to share with you the approach I've followed. If you need a live demo, then check out my portfolio website https://findakshay.dev and scroll down.

If there is something you didn't understand or not working for you or can be improved, please let me know. You can reach out to me via Twitter or Linkedin. You can also email me at akshay.milmile@gmail.com. You can check out some other projects that I've worked in past on my Github.
So that is it guys. Thank you for reading the article. Wish you a happy new year. Peace out ✌.