July 6, 2020
Build REST APIs with Go and Gin
@anthonycorletti

I've been building backend software applications primarily with Ruby and Python for the past five to six years, and have wanted to learn and build more with go because of it's growing developer ecosystem, presence in cloud-native software, and general performance.

In this post, I'm going to review a simple backend api I built with gin.

Some of the topics and technologies this post will cover include, gin, gorm, and environment configs with viper.

Let's dive in 🌊

Gin is a high-performance micro-framework that can be used to build web applications and microservices. Gin makes it simple to build a request handling pipeline from modular, reusable pieces. Gin does this by allowing you to write middleware that can be plugged into one or more request handlers or groups of request handlers.

Let’s take a quick look at how a request is processed in Gin. The control flow for a typical web application, API server, or a microservice looks as follows:

  1. Request
  2. Route Parser
  3. Optional Middleware
  4. Route Handler
  5. Optional Middleware
  6. Response

When a request comes in, Gin first parses the route. If a matching route definition is found, Gin invokes the route handler and zero or more middleware in an order defined by the route definition. We will see how this is done in code later on.

I used go1.14.4 at the time of writing this post and this project uses go modules. I also started this project from thockin's awesome go-build-template.

Clone the project to checkout the code.

git clone https://github.com/anthonycorletti/gin-sampler

There's a bunch in here and I'm going to review it topic by topic.

Let's first look at how and why I've set things up.

At first, I was scowering the internet for a gin framework that was something like FastAPI for Python but for Go, and came up empty handed.

There are plenty of quick CRUD api tutorials out there that had plenty of ways to show how to build http request handlers and persist data but those didn't feel professional.

I wanted to build a sample api that could serve as a boilerplate with properly functioning CRUD handlers, an ORM (but be flexible enough to integrate raw SQL and migrations where needed), scalable code design, and docker integration for straight-forward cicd pipelining and deployment with kubernetes. Scowering hundreds and hundreds lines of code just to add a feature or build a container gets old.

After plenty of searching I ended up with top two resources being thockin's awesome go-build-template and the Data Access Object (DAO) pattern.

I chose the DAO pattern because it is flexible and modular enough to write raw sql, use Gorm, and also build in migrations separately. It could work for supporting multiple kinds of database connections too.

go-build-template had everything I needed to start, just checkout that Makefile! Plenty of build, clean, and push commands that can be woven into whatever CI you choose! And easy to customize as well since most steps are run with docker.

Next, checkout the cmd/gin-sampler directory.

You should see five main directories and one main.go file.

Their responsibilities are broken down like so

apis/       # => makes service requests and returns a response to the router
config/     # => contains config files for the app
daos/       # => contains data access objects for working with data
main.go     # => instantiates the app, middleware, routers, etc
models/     # => specifies data models using gorm, and schemas for http requests
services/   # => maps functions (e.g. DAOs) in an interface for apis to consume

For me, a simple dev flow to add a feature goes like this ...

First, let's add a create user route to our main router:

v1.POST("/users", apis.CreateUser)

Here, we've specified that when a POST request is sent to our gin instance at /users, we'll invoke our CreateUser api call. That CreateUser function looks a little like this:

// CreateUser creates a user
func CreateUser(c *gin.Context) {
	s := services.NewUserService(daos.NewUserDAO())
	var input models.UserCreate
	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
		return
	}
	if user, err := s.Create(&input); err != nil {
		log.Println(err)
		c.JSON(http.StatusInternalServerError, gin.H{
            "message": "Oops we failed to create the user."})
	} else {
		c.JSON(http.StatusOK, user)
	}
}

Notice that our api function needs the services object, NewUserService, and dao NewUserDAO to function properly. We can set those up respectively like so:

// NewUserService

type userDAO interface {
	Create(input *models.UserCreate) (*models.User, error)
}

// UserService contains a userDAO
type UserService struct {
	dao userDAO
}

// NewUserService creates a new UserService with the given user DAO.
func NewUserService(dao userDAO) *UserService {
	return &UserService{dao}
}

// Create takes input and instantiates it in a database
func (s *UserService) Create(input *models.UserCreate) (*models.User, error) {
	return s.dao.Create(input)
}
// NewUserDAO

// UserDAO persists user data in database
type UserDAO struct{}

// NewUserDAO creates a new UserDAO
func NewUserDAO() *UserDAO {
	return &UserDAO{}
}

// Create creates a user
func (dao *UserDAO) Create(input *models.UserCreate) (*models.User, error) {
	user := models.User{
		FirstName: input.FirstName,
		LastName:  input.LastName,
		UserName:  input.UserName,
		Email:     input.Email,
	}
	err := config.Config.DB.Create(&user).Error
	return &user, err
}

At this point you might be thinking "Why create all the packages, separate files, layers of functions and what not?" – well, when your application gets sufficiently big, it would become a maintainability nightmare if you had everything lumped together.

In my opinion, this separation is needed for clearer testability, as it’s much easier to test each layer (database access, data manipulation, APIs, etc) separately rather than all in one place. While we are on the topic of tests, I think I should have added some! Next time!

Let's build and run the app and create a user. You may need to create the postgres database first: createdb gin-sampler (if you're using postgres's cli tooling).

make build && ./bin/darwin_amd64/gin-sampler

And ping the service:

$ curl -sX GET "http://localhost:8080/health" | jq
{
  "message": "alive and kicking",
  "time": "2020-07-08T05:57:13.597145Z",
  "version": "local"
}

And create a user:

$ curl -sX POST "http://localhost:8080/api/v1/users" -d '{ "first_name": "alice", "last_name": "smith", "user_name": "asmith", "email": "asmmith@example.com"}' | jq
{
  "id": 2,
  "created_at": "2020-07-08T05:59:09.291244Z",
  "updated_at": "2020-07-08T05:59:09.291244Z",
  "first_name": "alice",
  "last_name": "smith",
  "user_name": "asmith",
  "email": "asmmith@example.com"
}

Retrieve ...

$ curl -sX GET "http://localhost:8080/api/v1/users/2" | jq
{
  "id": 2,
  "created_at": "2020-07-08T01:59:09.291244-04:00",
  "updated_at": "2020-07-08T01:59:09.291244-04:00",
  "first_name": "alice",
  "last_name": "smith",
  "user_name": "asmith",
  "email": "asmmith@example.com"
}

Update ...

$ curl -sX PATCH "http://localhost:8080/api/v1/users/2" -d '{ "email": "asmith@example.com" }' | jq
{
  "id": 2,
  "created_at": "2020-07-08T01:59:09.291244-04:00",
  "updated_at": "2020-07-08T02:00:56.676262-04:00",
  "first_name": "alice",
  "last_name": "smith",
  "user_name": "asmith",
  "email": "asmith@example.com"
}

Retrieve many ...

$ curl -sX GET "http://localhost:8080/api/v1/users" | jq
[
  {
    "id": 2,
    "created_at": "2020-07-08T01:59:09.291244-04:00",
    "updated_at": "2020-07-08T02:00:56.676262-04:00",
    "first_name": "alice",
    "last_name": "smith",
    "user_name": "asmith",
    "email": "asmith@example.com"
  }
]

And delete ...

$ curl -sX DELETE http://localhost:8080/api/v1/users/2 | jq
{
  "id": 2,
  "created_at": "2020-07-08T01:59:09.291244-04:00",
  "updated_at": "2020-07-08T02:00:56.676262-04:00",
  "first_name": "alice",
  "last_name": "smith",
  "user_name": "asmith",
  "email": "asmith@example.com"
}
$ curl -sX GET http://localhost:8080/api/v1/users | jq
[]

Note that the created_at and updated_at fields are being stored as timestamp with time zone because that's all that Gorm can do when working with time.Time objects so an alternative would be to use iso strings but eek I could just deal with a UTC casting here and there - I'll let you decide what you want to do from there.

This was a great little exercise but I'm probably not going to build anything super mission critical using gin and gorm anytime soon without making sure I can keep all my data's times in UTC, deliver comprehensive error messages for data validation, simple background task processing, and more. For mission critical stuff, I'm still going to stick to Rails, Sinatra, FastAPI, and Django for the time being.

Moreover, this api isn't fully featured, nor is it production ready, but I would recommend reading through the code to understand how to build modular APIs with Gin and Go if you're interested in starting to write more go.

In the future, I may come back and add a few more things to make this production ready. Some ideas I have to add are; replacing postgres with cockroach db and having gin-sampler deployed to a kubernetes cluster with cockroach db, secure everything with proper SSL, RBAC, a secrets manager, and the works, code coverage, idempotent data migrations, tests, multi-env kubernetes deployment, and swagger docs – oh and UTC timestamps.

If you're coming from another programming ecosystem like Ruby or Python, building software with go can be a bit daunting at first, but is just as fun, and opens up new perspectives on building applications that offer performance benefits right from the start.