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.
1package main
2
3import (
4 "fmt"
5 "net/http"
6 "log"
7 "github.com/julienschmidt/httprouter"
8)
9
10func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
11 fmt.Fprint(w, "Welcome!\n")
12}
13
14func main() {
15 router := httprouter.New()
16 router.GET("/", Index)
17 log.Fatal(http.ListenAndServe(":8080", router))
18}
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.
1type Book struct {
2 ISDN string `json:"isdn"`
3 Title string `json:"title"`
4 Author string `json:"author"`
5 Pages int `json:"pages"`
6}
Our main.go file now looks like:
1func main() {
2 router := httprouter.New()
3 router.GET("/", Index)
4 router.GET("/books", BookIndex)
5 router.GET("/books/:isdn", BookShow)
6 log.Fatal(http.ListenAndServe(":8080", router))
7}
Now if you try to request GET https://localhost:8080/books, you get the following response:
1{
2 "meta": null,
3 "data": [
4 {
5 "isdn": "123",
6 "title": "Silence of the Lambs",
7 "author": "Thomas Harris",
8 "pages": 367
9 },
10 {
11 "isdn": "124",
12 "title": "To Kill a Mocking Bird",
13 "author": "Harper Lee",
14 "pages": 320
15 }
16 ]
17}
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:
1func TestIndex(t *testing.T) {
2 router := httprouter.New()
3 router.GET("/", Index)
4
5 req, _ := http.NewRequest("GET", "/", nil)
6 rr := httptest.NewRecorder()
7 router.ServeHTTP(rr, req)
8
9 if status := rr.Code; status != http.StatusOK {
10 t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
11 }
12}
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.