DEV Community

Cover image for Attempting to Learn Go - Now Sending REST Requests
Steve Layton
Steve Layton

Posted on

Attempting to Learn Go - Now Sending REST Requests

Forget GET, let's POST

So far, we've gone through a couple small examples on how to request data from a remote REST API. This time, however, we're going to explore how to submit data to a remote endpoint. You could probably use something like go-swagger as mentioned by @bgadrian to do all this and probably would in a production setting. For learning purposes though, we're going to just tackle what we can by using the standard library or small packages I've written that use the standard library.

This time we're going to look at sending an email using the MailGun API. I choose MailGun because is relatively straightforward and well documented - and I happen to have an account already. If you are going to follow along, you should sign-up.


GOPATH

But first! I was hoping to not really ever getting into GOPATH since it can be a little weird depending on the OS you are running. With that in mind, I'll the wiki cover most of the details and just touch on it as little as possible. For the most part, I'm assuming if you are reading this you likely already have Go installed and ready to go. I tried to put this project together a little differently as I wanted to start putting together some reusable packages we can hopefully draw on in future posts. With this in mind, we're going to write a very simple "send-only" MailGun client. My first step was to create a directory in my GOPATH (which for me is /Users/steve/Code/go) in my github.com/shindakun directory. Maybe I should have put it directly in the ATLG repo since it will end up there anyway... Or convert it to a Go Module... I'll worry about that later I suppose, let's make those directories.

mkdir $GOTPATH/github.com/shindakun/mailgunner
mkdir $GOTPATH/github.com/shindakun/mailgunner/client

I'm going to cover the code for the "client" first. Remember that we're attempting to do as much as we can with just the standard library - this is why we're not importing an existing MailGun client for Go. In our client directory create a main.go file and start setting up our package.

Quick aside - I recommend using VSCode and the ms-vscode.go plugin ... or not - use whatever editor your comfortable with.

package client

import (
  "net/http"
  "net/url"
  "strings"
)

There are not going to be too many moving parts to the client so we'll only need a few imports. strings and net/url will help get our data ready to add to the request and net/http will take of actually putting the HTTP request together.

Imports out of the way, there are a couple ways I could see doing the next part. But, I think the one presented here makes the most sense - in my mind at least. First, we declare a struct for MgClient, this struct will hold the API URL, our API token and http.Client.

NewMgClient() will take in the API URL and Token and return our struct. This setup allows us to use the package in multiple projects with different MailGun accounts, we can just pass it around as needed.

type MgClient struct {
  MgAPIURL string
  MgAPIKey string
  Client   *http.Client
}

func NewMgClient(apiurl, apikey string) MgClient {
  return MgClient{
    apiurl,
    apikey,
    http.DefaultClient,
  }
}

The real heavy lifting of the client package comes next with FormatEmailRequest(). It is a method of MgClient so it's passed back to the caller with the return from NewMgClient() - we'll look at that later. There are a few different things happening here so I've split the function in half to cover each (the full code listing will either be below or over on GitHub). The data object uses url.Values{} allowing us to put together our key-value pairs for what is ultimately a form submission. We're passing in all our variable values when we call the function.

func (mgc *MgClient) FormatEmailRequest(from, to, subject, body string) (r *http.Request, err error) {
  data := url.Values{}
  data.Add("from", from)
  data.Add("to", to)
  data.Add("subject", subject)
  data.Add("text", body)

With our email details out of the way, we need to build our HTTP request. This is similar to what we had been doing previously. Notice, however, that we are using http.MethodPost and not http.MethodGet. Additionally, we are using strings.NewReader() to create an io.Reader into which we pass data.Encode(). .Encode() simply URL encodes our key-value pairs so we'll get something like to=emailaddress&from=someotheraddress and so on. We then set the Basic Authorization header and the Content-Type header. The format in this case is not JSON but a URLencoded form.

  r, err = http.NewRequest(http.MethodPost, mgc.MgAPIURL+"/messages", strings.NewReader(data.Encode()))
  if err != nil {
    return nil, err
  }
  r.SetBasicAuth("api", mgc.MgAPIKey)
  r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
  return r, nil
}

And that's about it for "client" package! Simple right? I'll try to circle back around to this code in some future post where we add a test or two. You can see I've already begun to think about that by making sure we return from FormatEmailRequest() with an error if something goes wrong. The thinking here is that we want to error in the package but pass it back out so whoever is using the package can handle it however they choose.


Mailgunner

Now we'll go back up to our main mailgunner directory and create a new main.go file. Our imports are pretty clean, consisting of only fmt and io/ioutil, from the standard library, plus our new client package and one other. I made a brief allusion to the envy package in my Slackbot post it is a very small module which simply gets an environment variable or returns an error. Using ENV variables is a nice way to keep API keys out of Git repos. The code for "ENVy" could easily be included (it's only 5 lines after all) but I think this keeps everything a bit cleaner. Rather the write it over an over it becomes its own package. Below you can see envy.Get() being used to get the MailGun API key from the MGKEY environment variable.

package main

import (
  "fmt"
  "io/ioutil"

  "github.com/shindakun/envy"
  "github.com/shindakun/mailgunner/client"
)

func main() {
  mgKey, err := envy.Get("MGKEY")
  if err != nil {
    panic(err)
  }

With the basic setup out of the way, we're going to get to the good stuff. First, we'll create a new MgClient{} and store it in mgc. We then call mgc.FormatEmailRequest() with our test message. You could also move the API URL up out of the code into an ENV variable or a const as well if you want.

  mgc := client.NewMgClient("https://api.mailgun.net/v3/youremaildomain.com",
    mgKey)

  req, err := mgc.FormatEmailRequest("<Name> some@email.domain",
    "other@email.domain", "Test email", "This is a test email!")
  if err != nil {
    panic(err)
  }

From here on out the code will resemble our previous examples for the most part. We make the actual request with mgc.Client.Do(), read the []byte data out of res.Body, and finally convert to a string and print.

  res, err := mgc.Client.Do(req)
  if err != nil {
    panic(err)
  }
  defer res.Body.Close()

  body, err := ioutil.ReadAll(res.Body)
  if err != nil {
    panic(err)
  }

  fmt.Println(string(body))
}

And when all is said and done our output should look something like this.

$ MGKEY=key-key go run main.go
{
  "id": "<20181206152053.2.AEFB08ACDA47726E@youremaildomain.com>",
  "message": "Queued. Thank you."
}

Our email as it appears in Gmail

Next time

We're getting closer to doing something with the parts we've been laying out - not quite there but close. Next, we'll be modifying our API "getter" code from a couple posts back to pull down a list of a list of users from a remote endpoint. Once we have the users it should be a short trip to get the code in place to send everyone an email.


You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.



Top comments (2)

Collapse
 
shindakun profile image
Steve Layton

Yeah, when you are releasing a package it's a good idea to provide alternatives and just fall back to http.DefaultClient. For such a simple example I decided not to. You can see a good example of providing a fall back in a follow-up post. dev.to/shindakun/attempting-to-lea...