Build Your First API Server with httprouter in Golang
I turned into a Gopher about 10 months ago and have never looked back. Like many other gophers, I quickly found the simple features of the language quite useful to quickly build fast and scalable software. When I was initially picking up Go, I was playing around with different multiplexers available to be used as an API server.
If you are coming from a Rails background like me, you probably would also have struggled in building all the features which one can get from Web Frameworks. Coming back to the multiplexers, I found 3 to be quite useful: Gorilla mux, httprouter and bone (ordered in ascending order of their performance). Even though bone had the best performance and a simpler handler signature, it was not mature enough to be used in a production environment yet. So I ended up using httprouter.
In this tutorial, I would build a simple REST API server with httprouter. In case you feel lazy and just want the code, you can directly check my GitHub repository.
Let us begin.
First: Create a Basic Endpoint#
Index is a handler function and needs to have three input parameters. This handler is then registered to the path GET / in the main function.
package main
import (
"fmt"
"net/http"
"log"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func main() {
router := httprouter.New()
router.GET("/", Index)
log.Fatal(http.ListenAndServe(":8080", router))
}
Now compile and run your program and go to http://localhost:8080 to see your API server in action.
Adding More Complexity#
We now have an entity called Book which can be uniquely identified with the field ISDN. Let us create a couple more actions: GET /books and GET /books/:isdn representing the Index and Show actions respectively.
type Book struct {
ISDN string `json:"isdn"`
Title string `json:"title"`
Author string `json:"author"`
Pages int `json:"pages"`
}
Our main.go file now looks like:
func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/books", BookIndex)
router.GET("/books/:isdn", BookShow)
log.Fatal(http.ListenAndServe(":8080", router))
}
Now if you try to request GET https://localhost:8080/books, you get the following response:
{
"meta": null,
"data": [
{
"isdn": "123",
"title": "Silence of the Lambs",
"author": "Thomas Harris",
"pages": 367
},
{
"isdn": "124",
"title": "To Kill a Mocking Bird",
"author": "Harper Lee",
"pages": 320
}
]
}
These were the two entries of books which we hardcoded into the main function.
Refactoring the Code#
So far we have all the code in just one file, main.go. Let us move them to separate files. We now have a directory structure:
.
├── handlers.go
├── main.go
├── models.go
└── responses.go
- Move all the JSON response related structs to
responses.go - Move the handler functions to
handlers.go - Move the
Bookstruct tomodels.go
Writing Tests#
In Go, the *_test.go files are for tests. Let us create a handlers_test.go. We use the httptest package’s Recorder to mock the handlers:
func TestIndex(t *testing.T) {
router := httprouter.New()
router.GET("/", Index)
req, _ := http.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
}
Refactoring Further: DRY Handlers, Logging and Routes#
We still define all the routes in the main function, our handlers look verbose, we lack log messages in the terminal, and we still need a BookCreate handler.
First, let us DRY out handlers.go. I created two helper functions:
writeOKResponse— writes responses withStatusOKand a model or slice of modelswriteErrorResponse— writes a JSON error as a response for expected or unexpected errors
I also added populateModelFromHandler which unmarshals the body contents into any model you want. This is used in the BookCreate handler to populate a Book.
For logging, we create a Logger function that wraps around the handler functions and prints log messages before and after execution.
For routes, define all routes in one place in routes.go, and make a NewRouter function in router.go which reads all the routes and returns a usable httprouter.Router, wrapping each handler with the Logger function.
Your final directory structure should look like:
.
├── handlers.go
├── handlers_test.go
├── logger.go
├── main.go
├── models.go
├── responses.go
├── router.go
└── routes.go
For a larger project, you could organise into packages:
.
├── LICENSE
├── README.md
├── handlers
│ ├── books_test.go
│ └── books.go
├── models
│ └── book.go
├── store
│ └── *
├── lib
│ └── *
├── main.go
├── router.go
├── routes.go
└── logger.go
You can also put handlers, models, and all route functionalities under another package called app if you have a big monolithic server. Just keep in mind, Go is not like Java or Scala and there cannot be cyclic package calls — so take extra care with your package structure.
That is all and I hope this tutorial was useful. Cheers!
Originally published on Medium.