Build a simple JSON API in Go

Login to Access Code

This tutorial is more fun than most. My favorite use for Go is building API’s. It’s lightweight, has a robust standard library that does all the work you need it to, and is extremely straightforward. Many times, API’s are more of an anti-pattern and as such, a large framework or pattern focus can get in the way.

Setting up the project

I find that many tutorials don’t make the folder and file structure very apparent, so let’s address this first. You will want to create a new folder within your GOPATH directory. We’ll call ours go-api-tutorial.

*Tip: * you can find your GOPATH by opening up terminal and entering echo $GOPATH. If you do not have a path defined, you will probably want to look elsewhere on the web and come back when you have your environment setup correctly.

Inside your project folder, we will create the following files:

  • error.go - will be a struct to define and pass errors around
  • input.go - will be a struct to define the expected API input
  • output.go - will define our formatted API output
  • main.go - will handle our routes and errors

Defining Models in Go

Structs are awesome as models, and work well being contained in their own files. We will define three different “models” in our application: Error , Input and Output.

error.go

package main

type Error struct {
    Message string `json:"message"`
    Status  int    `json:"status"`
}

input.go

package main

type Input struct {
    Name string  `json:"name"`
    Cups float32 `json:"cups"`
}

output.go

package main

type Output struct {
    Description string  `json:"description"`
    Cereal      float32 `json:"cereal"`
    Milk        float32 `json:"milk"`
}

type Recommendations []Output

Controllers in Go

Our main.go file will serve as our controller for this application. It will setup the server and handle the routes accordingly:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
)

// Define a custom port, which is nice both for dev and production
var port string = os.Getenv("PORT")

func main() {
    if port == "" {
        log.Println("no port name provided, using 3000")
        port = "3000"
    }

    // Since this is a very minimal API, we should have all requests come through the root handler
    http.HandleFunc("/", root)
    log.Printf("Listening for connections on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

/**
 * POST /
 *
 * Handle all requests to the root url.
 * This expects a POST request with a JSON request body
 * the payload should contain the values defined in the Input struct
 */
func root(w http.ResponseWriter, r *http.Request) {
    // Set the content type to JSON regardless of the data returned
    w.Header().Set("Content-Type", "application/json")

    // Try to decode the data provided
    var cereal Input
    err := json.NewDecoder(r.Body).Decode(&cereal)

    // If an error is encountered, let's say so with a nice handler
    if err != nil {
        handleError("Error parsing data", 500, w)
        return
    }

    // Build our recommendations with some simple math
    recommendations := Recommendations{
        Output{
            Description: fmt.Sprint(cereal.Name, " with skim milk"),
            Cereal:      cereal.Cups,
            Milk:        cereal.Cups * .75,
        },
        Output{
            Description: fmt.Sprint(cereal.Name, " with 2% milk"),
            Cereal:      cereal.Cups,
            Milk:        cereal.Cups * 1,
        },
        Output{
            Description: fmt.Sprint(cereal.Name, " with whole milk"),
            Cereal:      cereal.Cups,
            Milk:        cereal.Cups * 1.25,
        },
    }

    // Encode the recommendations array of structs into JSON
    json.NewEncoder(w).Encode(recommendations)
}

/**
 * Handle Errors
 *
 * Response codes are more than enough in most applications, however, I find that a nice JSON response body
 * with a more verbose description of the error and code are tremendously helpful to developers.
 */
func handleError(message string, code int, w http.ResponseWriter) {
    err := Error{Message: message, Status: code}
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(err)
}