Building a Book Store API in Golang With Gin
Tags: go, gin, api
Published: 2022-03-18 04:16:18


## Introduction

Go community believes that we need no framework to develop web services. And I agree with that. When you work without using any framework, you learn the ins and outs of development. But once you learn how things work, you must reside in a community and don't reinvent the wheel. 

I have created POCs in Go before and I had to deal with HTTP headers, serializing/deserializing, error handling, database connections, and whatnot. But now I've decided to join the Gin community as it is one of the widely accepted in the software development community.


Although I'm writing this post as a standalone article and would keep things simple here. But I want to continue building on these examples to have authentication, authorization, databases (including Postgres, ORM), swagger, and GraphQL covered. Will be interlinking the posts when I create them.

## Why Gin

<img src="https://d2kbulfv42d43j.cloudfront.net/2022/Q1/gin-golang-banner.jpg" />


There are numerous reasons you may want to use Gin. If you ask me, I'm a big fan of Gin's sensible defaults.

Another thing I like about Gin is that it's an entire framework. You don't need a separate multiplexer and a separate middleware library and so on. On top of that, there are many common things already available that you don't have to reinvent. It does enhance our productivity. Although I'm not using it in production, I already have started to feel it.

## Hello World in Gin

```go
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.New()

	router.GET("/ping", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})

	router.Run()
}

```

Let's get familiar with Gin a little bit. 

```
router := gin.New()
```

This creates an instance of  `Engine`. `gin.Engine` is the core of gin. It also acts as a router and that is why we have put that Engine instance into a variable called `router`. 

```
router.GET("/ping", func(ctx *gin.Context) {
```

This binds a route `/ping` to a handler. In the example above, I've created an anonymous function, but it could be a separate function as well. The thing to note here is the parameter to this function. `*gin.Context`. `Context` is yet another important construct besides Engine. `Context` has almost 100 methods attached to it. A newcomer should be spending most of their time understanding this `Context` struct and their methods. 

Let's now look at the next few lines:

```
		ctx.JSON(http.StatusFound, gin.H{
			"message": "pong",
		})
```

One of the `*gin.Context` method is `JSON`. This method is used to send a JSON response to the client. Meaning that it automatically sets response's `Content-Type` to `application/json`. JSON method takes an HTTP status code and a map of response. `gin.H` is an alias to `map[string]interface{}`. So basically we can create an object which can have string key and whatever value we want.

Next is:

```
router.Run()
```

Engine.Run simply takes our router along with the route handler and binds it to http.Server. The default port is `8080`, but if you want, you can have another address passed here. 

## The Book Store API

I've already done a POC on bookstore before, at that time, I wanted to prototype a connection between MongoDB and Go. But this time my goal is to have Postgres and GraphQL incorporated.

So first of all, I'd like you to set up a directory structure like this:

```
$ tree
.
├── db
│   └── db.go
├── go.mod
├── go.sum
├── handlers
│   └── books.go
├── main.go
└── models
    └── book.go
```

And let's start filling up those files. 

### db/db.go

```go
package db

import "github.com/santosh/gingo/models"

// Books slice to seed book data.
var Books = []models.Book{
	{ISBN: "9781612680194", Title: "Rich Dad Poor Dad", Author: "Robert Kiyosaki"},
	{ISBN: "9781781257654", Title: "The Daily Stotic", Author: "Ryan Holiday"},
	{ISBN: "9780593419052", Title: "A Mind for Numbers", Author: "Barbara Oklay"},
}
```

Instead of going into the complexity of setting up a database right now, I've decided to use an in-memory database for this post. In this file, I've seeded `db.Books` slice with some books.

If `models.Book` makes, you curious, the next file is that only.

### models/book.go

```go
package models

// Book represents data about a book.
type Book struct {
	ISBN   string  `json:"isbn"`
	Title  string  `json:"title"`
	Author string  `json:"author"`
}
```

Nothing fancy here, we only have 3 fields as of now. All of them are strings and with struct tags.

Let us see our main.go before we go onto handlers.go.

### main.go

```go
package main

import (
	"github.com/gin-gonic/gin"
	"github.com/santosh/gingo/handlers"
)

func setupRouter() *gin.Engine {
    router := gin.Default()
	router.GET("/books", handlers.GetBooks)
	router.GET("/books/:isbn", handlers.GetBookByISBN)
	// router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
	// router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
	router.POST("/books", handlers.PostBook)

    return router
}

func main() {
    router := setupRouter()
	router.Run(":8080")
}
```

Almost similar to the hello world example we saw above. But this time we have `gin.Default()` instead of `gin.New()`. The `Default` comes with defaults which most of us would like to have. Like logging middleware.

Frankly speaking, I haven't used much of Gin's middleware yet. But it's damn simple to create your middlewares. I'll put some links at the bottom of the post for your exploration. But for now, let's look at our handlers.

### handlers/books.go

```go
package handlers

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/santosh/gingo/db"
	"github.com/santosh/gingo/models"
)

// GetBooks responds with the list of all books as JSON.
func GetBooks(c *gin.Context) {
	c.JSON(http.StatusOK, db.Books)
}

// PostBook takes a book JSON and store in DB.
func PostBook(c *gin.Context) {
	var newBook models.Book

	// Call BindJSON to bind the received JSON to
	// newBook.
	if err := c.BindJSON(&newBook); err != nil {
		return
	}

	// Add the new book to the slice.
	db.Books = append(db.Books, newBook)
	c.JSON(http.StatusCreated, newBook)
}

// GetBookByISBN locates the book whose ISBN value matches the isbn
func GetBookByISBN(c *gin.Context) {
	isbn := c.Param("isbn")

	// Loop over the list of books, look for
	// an book whose ISBN value matches the parameter.
	for _, a := range db.Books {
		if a.ISBN == isbn {
			c.JSON(http.StatusOK, a)
			return
		}
	}
	c.JSON(http.StatusNotFound, gin.H{"message": "book not found"})
}

// func DeleteBookByISBN(c *gin.Context) {}

// func UpdateBookByISBN(c *gin.Context) {}
```

The real juice is in this handlers file. This might need some explanation.

`handlers.GetBooks`, which is bound to `GET /books` dumps the entire book slice.

`handlers.GetBookByISBN`, which is bound to `GET /books/:isbn` does the same thing, but it also accepts `isbn` as a URL parameter. This handler scans the entire slice and returns the matched book. Scanning a large slice would not be the most optimal solution, but don't forget that we'll be implementing a real database while we continue to develop this bookstore.

The most interesting one here is `handlers.PostBook`, which is bound to `POST /books`. `c.BindJSON` is the magic method, which takes in the JSON from the request and stores it into previously created `newBook` struct. Later on

## Tests

We need a little change here at the moment. We need to remove these contents from main.go:

```diff
@@ -1,17 +1,9 @@
 package main
 
-import (
-       "github.com/gin-gonic/gin"
-       "github.com/santosh/gingo/handlers"
-)
+import "github.com/santosh/gingo/routes"
 
 func main() {
-       router := gin.Default()
-       router.GET("/books", handlers.GetBooks)
-       router.GET("/books/:isbn", handlers.GetBookByISBN)
-       // router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
-       // router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
-       router.POST("/books", handlers.PostBook)
+       router := routes.SetupRouter()
 
        router.Run(":8080")
 }
```

And put it into a new file. 

#### routes/roures.go

```go
package routes

import (
	"github.com/gin-gonic/gin"
	"github.com/santosh/gingo/handlers"
)

func SetupRouter() *gin.Engine {
	router := gin.Default()
	router.GET("/books", handlers.GetBooks)
	router.GET("/books/:isbn", handlers.GetBookByISBN)
	// router.DELETE("/books/:isbn", handlers.DeleteBookByISBN)
	// router.PUT("/books/:isbn", handlers.UpdateBookByISBN)
	router.POST("/books", handlers.PostBook)

	return router
}

```

I have changes that make sense to you. We did this because we need to start the server from our tests.

Next, we create a `books_test.go` in handlers.

#### handlers/books_test.go

```go
package handlers_test

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/santosh/gingo/models"
	"github.com/santosh/gingo/routes"
	"github.com/stretchr/testify/assert"
)


func TestBooksRoute(t *testing.T) {
	router := routes.SetupRouter()

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/books", nil)
	router.ServeHTTP(w, req)

	assert.Equal(t, 200, w.Code)
	assert.Contains(t, w.Body.String(), "9781612680194")
	assert.Contains(t, w.Body.String(), "9781781257654")
	assert.Contains(t, w.Body.String(), "9780593419052")
}

func TestBooksbyISBNRoute(t *testing.T) {
	router := routes.SetupRouter()

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("GET", "/books/9781612680194", nil)
	router.ServeHTTP(w, req)

	assert.Equal(t, 200, w.Code)
	assert.Contains(t, w.Body.String(), "Rich Dad Poor Dad")
}


func TestPostBookRoute(t *testing.T) {
	router := routes.SetupRouter()

	book := models.Book{
		ISBN: "1234567891012",
		Author: "Santosh Kumar",
		Title: "Hello World",
	}

	body, _ := json.Marshal(book)

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", "/books", bytes.NewReader(body))
	router.ServeHTTP(w, req)

	assert.Equal(t, 201, w.Code)
	assert.Contains(t, w.Body.String(), "Hello World")
}
```

Also, again, pretty much self-explanatory. I don't think the above code needs any explanation. We are testing for response codes and response bodies for a specific string.

Let's also run the tests and check how it goes:

```
$ go test ./... -cover
?       github.com/santosh/gingo        [no test files]
?       github.com/santosh/gingo/db     [no test files]
ok      github.com/santosh/gingo/handlers       (cached)        coverage: 83.3% of statements
?       github.com/santosh/gingo/models [no test files]
?       github.com/santosh/gingo/routes [no test files]
```

## Exercise

Yeah, let's this blog post more interesting by adding some interactivity. I have some tasks for you, which you need to solve on your own. Please have a try on them. They are:

1. Implement `DeleteBookByISBN` and `UpdateBookByISBN` handlers and enable them.
2. Write tests for handlers mentioned above.
3. Our tests are very basic. So are our handlers. We are not doing any error handling. Add error handling to handlers and write tests to validate them.

## Conclusion

We have seen how simple is it to create a hello world application in Gin. But this journey does not end here. I'll come back with more tutorials next time. Until then, goodbye.

**Related Link**

* <https://sosedoff.com/2014/12/21/gin-middleware.html>
* <https://santoshk.dev/posts/2020/sending-post-request-in-go-with-a-body/>