Building a JSON API in Go

February 5th 2018
by Cory Finger

Updated on October 5th 2018

I've recently been building more and more complex things with the Go programming language. It's been a lot of fun!

As someone who's been burned by multi-threaded programming on more than one occasion, I can appreciate the simplicity of the thread-safe utilities built into the language. I'm finding the speed, compilation and type system just delightful.

Anywho, the first thing I always do when learning a programming language is build a JSON API. I just built one that powers a social-share URL shortener that you'll see soon!

I thought I'd share the steps I took to set up a basic API in Go!

Want to read more posts like this? Subscribe to the blog! Sign in with Github

Install Go

This post assumes that you've already installed Go on your machine.

Setting up the project folder

Let's start by creating a new folder that'll contain our API:

$ mkdir -p $GOPATH/src/github.com/YOUR_GITHUB_USERNAME/my-go-api
$ cd $GOPATH/src/github.com/YOUR_GITHUB_USERNAME/my-go-api

Through development, I've found that the most convenient place to keep Go projects is as a subdirectory to your $GOPATH. That's where the Go compiler and the surrounding tools expect projects to be so that they can easily locate dependencies, binaries and other useful information.

While we're here, let's create a src directory where our source code for the API will live:

$ mkdir src

Installing dep

dep is a tool we'll be using to download and keep track of our API's dependencies.

If you've ever used Ruby, it serves the same purpose as a Gemfile. If you've build a Javascript app, it serves a similar purpose to package.json.

There's a number of popular Go dependency management tools:

We're going to use dep because it is scheduled to become the official Go dependency management tool and the other tools are starting to recommend using it.

So, how can we install dep?

$ go get -u github.com/golang/dep/cmd/dep

That'll put dep in your $GOPATH/bin folder. So long as that's in your $PATH, you should now be able to say which dep and see that it's installed!

Want to read more posts like this? Subscribe to the blog! Sign in with Github

Gorilla Toolkit

We're going to be using an awesome library developed by a Github user named gorilla. The creator of the Gorilla Web Toolkit:

http://www.gorillatoolkit.org/

It'll handle taking the API requests and routing them to the right place. It does this through its multiplexer (mux).

The reason we'll be using mux in particular is that it supports the path parameters that we can make to make easy-to-understand URLs.

We'll also be using the handlers package in a future section of this post that deals with setting up CORS handling for our API.

Initialize dep

First, let's initialize dep:

$ dep init

This will create a directory called vendor. dep will use this directory to store the source code files of our dependencies.

It will also create files called Gopkg.json and Gopkg.toml in the root directory.

  • Gopkg.json is where dep will document all our API's dependencies and the versions our projects require.

  • Gopkg.toml is where the hand-editted options for dep are stored.

Build an Endpoint

Let's get started by building an endpoint! We could create a running server that doesn't have an API endpoint - But where's the fun in that?

We're going to build an endpoint that a load balancer could use to do a health check on the server. It also serves the purpose of giving a straightforward explanation of building and endpoint and will give us something we can see in our browser to check it out!

Create a file src/main.go with the following contents:

package main

import (
    "encoding/json"
    "fmt"
    "github.com/gorilla/mux"
    "log"
    "net/http"
)

func main() {
    var router = mux.NewRouter()
    router.HandleFunc("/healthcheck", healthCheck).Methods("GET")

    fmt.Println("Running server!")
    log.Fatal(http.ListenAndServe(":3000", router))
}

func healthCheck(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode("Still alive!")
}

Let's break this down by section real quick:


import (
    "fmt"
    "log"
    "encoding/json"
    "net/http"
    "github.com/gorilla/mux"
)

First, we import the libraries that we'll be making use of in this file.

  • fmt is what we'll be using to print to STDOUT (the console)

  • log is used to log when the server exits

  • encoding/json is for creating our JSON responses

  • net/http will give us the representations of HTTP requests, responses, and be responsible for running our server

  • github.com/gorilla/mux will be our router that will take requests and decide what should be done with them


func main() {
    var router = mux.NewRouter()
    router.HandleFunc("/healthcheck", healthCheck).Methods("GET")

    fmt.Println("Running server!")
    log.Fatal(http.ListenAndServe(":3000", router))
}

What we do here is:

  1. Create a router that will route HTTP requests to Go functions

  2. Set up a function that will handle all requests to the /healthcheck path

  3. Print a friendly message to STDOUT to show that our server is running

  4. Start listening on port 3000 with our new router.

  5. Log the reason the server gives when it eventually exits


Lastly, we have the healthcheck function itself:

func healthCheck(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode("Still alive!")
}

This part of the code takes in a http.ResponseWriter (w) that we can use to respond to the client.

It also takes in a http.Request (r) that will allow us to pull details of the request to better know what the client wants. We'll ignore it in this part of the blog post. But we'll come back to that later!

We're using a JSON encoder to turn our response into proper JSON and then we're writing it to the http.ResponseWriter to send it to the client.

Download dependencies

Next, we'll download the gorilla libraries that we imported in the code:

$ dep ensure

This command will recursively look for all our .go files, look at the imports, and add our depedencies to our Gopkg.lock file.

It will also download the source code for those dependencies to the new vendor/github.com/gorilla directory.

Whenever you build your project, these dependencies will be compiled into the binary so that you can run the binary without any external requirements.

Running our API

To start our new API, all we need to do is run the following command:

$ go run src/*.go

Open your browser and point it at http://localhost:3000/healthcheck to check it out!

Handling Parameters

Let's make this a little more complicated and handle some GET parameters:

package main

import (
    "encoding/json"
    "fmt"
    "github.com/gorilla/mux"
    "log"
    "net/http"
)

func main() {
    var router = mux.NewRouter()
    router.HandleFunc("/healthcheck", healthCheck).Methods("GET")
    router.HandleFunc("/message", handleQryMessage).Methods("GET")
    router.HandleFunc("/m/{msg}", handleUrlMessage).Methods("GET")

    fmt.Println("Running server!")
    log.Fatal(http.ListenAndServe(":3000", router))
}

func handleQryMessage(w http.ResponseWriter, r *http.Request) {
    vars := r.URL.Query()
    message := vars.Get("msg")

    json.NewEncoder(w).Encode(map[string]string{"message": message})
}

func handleUrlMessage(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    message := vars["msg"]

    json.NewEncoder(w).Encode(map[string]string{"message": message})
}

func healthCheck(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode("Still alive!")
}

Now that we have a dynamic API, the sky is the limit!

We can break the changes down step-by-step:


router.HandleFunc("/message", handleQryMessage).Methods("GET")
router.HandleFunc("/m/{msg}", handleUrlMessage).Methods("GET")

The first line here is setting up a simple GET route, just like the health check route we had earlier.

The second part is setting up a route that takes in path parameters.

These parameters are pretty much the same as GET parameters. But they allow us to make the pretty-looking, easier-to-understand routes that CRUD APIs are known for.


func handleQryMessage(w http.ResponseWriter, r *http.Request) {
    vars := r.URL.Query()
    message := vars.Get("msg")

    json.NewEncoder(w).Encode(map[string]string{"message": message})
}

func handleUrlMessage(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    message := vars["msg"]

    json.NewEncoder(w).Encode(map[string]string{"message": message})
}
  • handleQryMessage takes in a GET parameter from the URL and generates a JSON response the repeats the variable back to the client.

You can try this out by starting the server with go run src/*.go and then navigating to:

http://localhost:3000/message?msg=Hello%20World

  • handleUrlMessage takes in a path parameter from the URL and outputs a JSON response that repeats the variable back to the client..

You can try this out by starting the server with go run src/*.go and then navigating to:

http://localhost:3000/m/HelloWorld


Note: You can also handle form POST parameters via the FormValue method:

message := r.FormValue("msg")

Setting up CORS

We now have a working API. But you can't make a kickass Javascript application without having CORS set up on your API server.

CORS tells the browser that it's OK for JS to communicate with your server.

gorilla seems to have thought of that and made that easy for us!

package main

import (
    "encoding/json"
    "fmt"
    "github.com/gorilla/mux"
    "github.com/gorilla/handlers"
    "log"
    "net/http"
)

func main() {
    var router = mux.NewRouter()
    router.HandleFunc("/healthcheck", healthCheck).Methods("GET")
    router.HandleFunc("/message", handleQryMessage).Methods("GET")
    router.HandleFunc("/m/{msg}", handleUrlMessage).Methods("GET")

    headersOk := handlers.AllowedHeaders([]string{"Authorization"})
    originsOk := handlers.AllowedOrigins([]string{"*"})
    methodsOk := handlers.AllowedMethods([]string{"GET", "POST", "OPTIONS"})

    fmt.Println("Running server!")
    log.Fatal(http.ListenAndServe(":3000", handlers.CORS(originsOk, headersOk, methodsOk)(router)))
}

func handleQryMessage(w http.ResponseWriter, r *http.Request) {
    vars := r.URL.Query()
    message := vars.Get("msg")

    json.NewEncoder(w).Encode(map[string]string{"message": message})
}

func handleUrlMessage(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    message := vars["msg"]

    json.NewEncoder(w).Encode(map[string]string{"message": message})
}

func healthCheck(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode("Still alive!")
}

We first have to import the new library that we'll use for CORS:

import (
    ...
    "github.com/gorilla/handlers"
    ...
)

Then we set the request attributes that are allowed to be sent:

headersOk := handlers.AllowedHeaders([]string{"Authorization"})
originsOk := handlers.AllowedOrigins([]string{"*"})
methodsOk := handlers.AllowedMethods([]string{"GET", "POST", "OPTIONS"})
  • headersOK decides what headers can be sent via Javascript
  • originsOk allows for all origins to make JS requests
  • methodsOk is a whitelist for what types of requests JS can send

log.Fatal(http.ListenAndServe(":3000", handlers.CORS(originsOk, headersOk, methodsOk)(router)))

We then insert the handlers.CORS middleware between the router and our http server.

The middleware will add the proper headers to the server's responses and allow front-end Javascript to use it without issue.

Run dep ensure again to install the new dependency and go run src/*.go again to try out the new API!

gofmt and goimports

Whenever you save Go code, it's good practice to run a formatting tool on the file.

This formatting tool will make sure that the code is in the format that Golang recommends and helps everyone to write code that fits with code written by other programmers. It's especially a good idea if writing open-source software.

gofmt

gofmt is the tool that comes bundled with Golang:

$ gofmt src/main.go

You'll notice that this just outputs the contents of the file. This output is formatted so that tabs are used instead of spaces and other community-accepted practices.

To clean up the file on disk instead of outputting to console, you can use the -w flag:

$ gofmt -w src/main.go

That'll clean up the file and save it without outputting anything to console. A lot of people set this up to run on their computer whenever they save a Go file.

goimports (the new gofmt)

So gofmt is awesome. But let's install a tool called goimports:

$ go get golang.org/x/tools/cmd/goimports

This tool does everything gofmt does, but it also cleans up your project imports. It removes unused ones and adds missing ones to the code.

$ goimports -w src/main.go

This has become the new standard used by the Go community for all .go files!

Enjoy your API!

Hope you like your new API! Go and make something cool with it!

Want to read more posts like this? Subscribe to the blog! Sign in with Github