Building a Book Store API in Golang With Gin

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

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

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

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

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

package mainimport (
"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

package handlersimport (
"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:

@@ -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

package routesimport (
"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

package handlers_testimport (
"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

Originally published at https://santoshk.dev on March 17, 2022.

--

--

--

Full Stack Developer. Pythonist. Gopher.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Intro To Basic OOP In Ruby Class and Objects — Part 1

Ruby Programming Language

The End is here!

Easy integration testing with Codefresh pipelines

About Dropp Land

How to Elastic SIEM (part 1)

Software Development: Progressing from Average to World class

✍🏻Industry use cases of Jenkins

Advent of Code 2020: Day 10

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Santosh Kumar

Santosh Kumar

Full Stack Developer. Pythonist. Gopher.

More from Medium

How to Connect PostgreSQL Database with Golang

SimpleApp — Connect To MongoDB (Part II)

MySQL: Init GORM from YAML with rk-boot

A First Look into Concurrency in Go