Bit Hacker Logo
Bit Hacker Logo
Build a REST API with Go and Gorilla Mux

Build a REST API with Go and Gorilla Mux

Go is very attractive when considering a backend API. Its multi-threading and concurrent processing make it an excellent choice for web services. Let's play around with it and setup a very basic REST API base project.

main.go

First we need to kick things off in our our main.go file. If you haven't already, create a rest-api folder in your GOPATH/src directory. Next, create a new main.app as a starting point to our application.

What is a GOPATH?

If you are unfamiliar with what the GOPATH is you may need to follow our guide on how to Install and Setup Go for Development on Mac or Windows.

package main

func main() {
	a := App{}
	a.Initialize()
	a.Run(":8000")
}

Inside the main.app we will be using a new App object to keep our application logic in its own area to make things a bit cleaner. We will define this functionality in app.go.

The App

Now that main.go is out of the way, it is time to create the App logic. This is initially organized within our Intitalize and Run methods. Initialize will setup our routes and Run will start the actual server responsible for handling requests.

package main

type App struct {
}

func (a *App) Initialize() {
}

func (a *App) Run(addr string) {
}

Routes

With the scaffolding done, we need a way of receiving our GET, POST and other REST requests and mapping them to a function. We could simply write our own library to match requests but rather than re-inventing the wheel we can use gorilla/mux.

Start by adding an importing gorilla/mux in app.go just below package main:

package main

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

Add a Router property to the App struct definition to hold our router.

type App struct {
    Router *mux.Router
}

Our first route

Now that mux is accessible, it is time to create our router along with our first route. Below, we initialize a new mux router and use the Handle method to define a route.

func (a *App) Initialize() {
  router := mux.NewRouter()
  router.Handle("/hello/", HelloWorldHandler() ).Methods("GET")
  a.Router = router
}

HelloWorldHandler()

Before creating our HelloWorldHandler() method, we are going to use some methods from the net/http library, so you will need to add it to your import:

With a proper IDE, these imports will get automatically added for you. Check out our article on how to Install and Setup Go for Development on Mac or Windows.

import (
	"github.com/gorilla/mux"
	"net/http"
)

The HelloWorldHandler is the logic behind the route. It returns an http.Handler which mux will call when the /hello/ is requested. Inside the handler we will return a []byte array from a string as our output.

func (a *App) Initialize() {
	// Initialize method already defined ...
}

func HelloWorldHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World"))
	})
}

func (a *App) Run(addr string) {
	// Run method already defined ...
}

With our router setup, we need to tell our Run method to start the http server.

func (a *App) Run(addr string) {
    http.ListenAndServe(addr, a.Router)
}

First Contact

At this point we can try our server to verify everything is running. If you haven't already, open a terminal or command prompt and run the following

Grab and install our third party packages:

$ go get .

Run the server

$ go run .

Open your browser and head to http://localhost:8000/hello/ and you should see:

GET http://localhost:8000/hello/

Hello, World

We have contact!

JSON

Output is great, but our API won't be much use if we didn't process input and spit out data structures with something like JSON.

Enter JSONResponse. This receives our http.ResponseWriter from our route handler and allows us set our response code. The magic here is with our JSON marshaling of the output which we pass as any object and transform it into JSON.

func JSONResponse(w http.ResponseWriter, code int, output interface{}) {
	// Convert our interface to JSON
	response, _ := json.Marshal(output)
	// Set the content type to json for browsers
	w.Header().Set("Content-Type", "application/json")
	// Our response code
	w.WriteHeader(code)

	w.Write(response)
}

Our new HelloWorldResponse object will be used to hold our response and show how to convert a Go struct to JSON. Can be placed anywhere in app.go.

type HelloWorldResponse struct {
	Message       string `json:"msg"`
	ProvidedName  string `json:"name"`
}

Now we can get our HelloWorldHandler to receive a some user input, manipulate the data and return an object back automatically formatted to JSON.

func HelloWorldHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		name := r.FormValue("name")

		response := HelloWorldResponse{
			ProvidedName: name,
			Message: fmt.Sprintf("Hello, %s!", name),
		}

		JSONResponse(w, 200, response)
	})
}

We have added a few new dependencies, so if your IDE has not added the imports, here is the updated list:

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

Try it out

Install packages and run the server

$ go get .
$ go run .

Open your browser and head to http://localhost:8000/hello/?name=Matt and you should see:

{
  "msg": "Hello, Matt!",
  "name": "Matt"
}

What about POST?

No REST API is complete without adding new entries to a database or removing old data. While database interaction is outside this article, let's explore defining routes to receive complex objects and using them in our handlers.

Let us start with a new request struct defining what our POST request is going to look like. We are going to accept an object with Age and Name as properties.

type HelloWorldRequest struct {
	Age     int    `json:"age"`
	Name    string `json:"name"`
}

To handle our request, we need a new handler. Below the HelloWorldHandler add a new HelloWorldPostHandler.

func HelloWorldPostHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		var helloRequest HelloWorldRequest
		decoder := json.NewDecoder(r.Body)
		if err := decoder.Decode(&helloRequest); err != nil {
			JSONResponse(w, http.StatusBadRequest, map[string]string{"error": "Invalid request"})
			return
		}
		defer r.Body.Close()

		response := HelloWorldResponse{
			ProvidedName: helloRequest.Name,
			Message:      fmt.Sprintf("Hello, %s! You are %d years old!", helloRequest.Name, helloRequest.Age),
		}

		JSONResponse(w, 200, response)
	})
}

This handler is very similar to the HelloWorldHandler with the exception of our request processing with the json.Decoder.

The decoder will look at the request body (r.Body) and attempt to decode it into the passed in object helloRequest which is the struct we defined in the previous step.

If the request can be decoded, our helloRequest variable holds the user input. If not we spit out a bad request response with our JSONResponse object.

Lastly, defer r.Body.Close() is called to free the the body resource for later use.

Add your route

Before we can fire off POST requests, we need to tell mux about our new route back in our Initialize method.

func (a *App) Initialize() {
	router := mux.NewRouter()
	router.Handle("/hello/", HelloWorldHandler()).Methods("GET")
	router.Handle("/hello/", HelloWorldPostHandler()).Methods("POST")
	a.Router = router
}

Try it out

Re-run the server

$ go run .

Using a command like curl, you are now set to POST away!

$ curl -d '{"Name":"Jimmy Bytes", "age":0}' -H "Content-Type: application/json" -X POST http://localhost:8000/hello/
{
  "msg":"Hello, Jimmy Bytes! You are 0 years old!",
  "name":"Jimmy Bytes"
}

If you don't have curl or are unfortunate enough to be on Windows, stuck with PowerShell's attempt at masking curl, you can use an application like Postman or Insomnia to test POST requests.

Go REST API

There you have it! Go has great multi-threading and concurrency making it a great choice for building web based REST API's. This should give you a great starting point to building out your very first Go based API.

Some follow up ideas would be to explore adding application logging to keep track of errors and a database connectivity. If you have any other ideas please share in the comments below!

Resources