How to Build a REST API with Golang using Gin and Gorm

February 25, 2020

This article was originally written for LogRocket.

Go is a very popular language for good reason. It offers similar performance to other “low-level” programming languages such as Java and C++, but its also incredibly simple, which makes the development experience delightful. Put simply, Go is a powerful language that puts together the best of both worlds.

What if we could combine a fast programming language with a speedy web framework to build a high-performance RESTful API that can handle a crazy amount of traffic?

After doing a lot of research to find a fast and reliable framework for this beast, I came across a fantastic open-source project called Gin. This framework is lightweight, well-documented, and, of course, extremely fast.

Unlike other Go web frameworks, Gin uses a custom version of HttpRouter. Which means it can navigate through your API routes faster than most frameworks out there. The creators also claim it can run 40 times faster than Martini, a relatively similar framework to Gin. You can see a more detailed comparison in this benchmark.

Although it may seem like the Holy Grail at first glance, this stack might not be the best option for your project, depending on the scenario.

Gin is a micro-framework that doesn’t come with a ton of fancy features out of the box. It only gives you the essential tools to build an API, such as routing, form validation, etc. So for tasks such as authenticating users, uploading files, and sending emails, you need to either install another third-party library or implement them yourself.

This can be a huge disadvantage for a small team of developers that needs to ship a lot of features in a very short time. Other web frameworks, such as Laravel and Ruby on Rails, might be more appropriate for such a team. These frameworks are opinionated, easier to learn, and provide a lot of features out-of-the-box, which enables you to develop a fully functioning web application in an instant.

If you’re part of a small team, this stack may be overkill. Having said that, if you have the appetite to make a long-term investment, you can really take advantage of the extraordinary performance and flexibility of Gin.

What we will build

In this tutorial, we’ll demonstrate how to build a bookstore REST API that provides book data and performs CRUD operations.

Before we get begin, I’ll assume that you:

  • Have Go installed on your machine
  • Understand the Go language
  • Have a general understanding of RESTful API

Let’s start by initializing a new Go module. This will enable us to manage the dependencies that are specifically installed for this project. Make sure you run this command inside your Go environment folder.

$ go mod init

Now let’s install some dependencies.

$ go get github.com/gin-gonic/gin github.com/jinzhu/gorm

After the installation is complete, your folder should contain two files: mod.mod and go.sum. Both contain information about the packages you installed, which when it comes to working with other developers. Whenever somebody wants to contribute to this project, they just have to run go mod download command on their terminal, and all of the required dependencies will be installed on their machine.

For reference, I published the entire source code of this project on my GitHub. Feel free to poke around or clone it onto your computer.

$ git clone https://github.com/rahmanfadhil/gin-bookstore.git

Setting up server

Let’s start by creating a “hello world” server inside main.go file.

main.go
package main

import (
  "net/http"
  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()

  r.GET("/", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"data": "hello world"})
  })

  r.Run()
}

We first need to declare the main function that will be triggered whenever we run our application. Inside this function, we initialize a new Gin router inside the r variable. And the reason why we use the Default router is that Gin provides you some useful middlewares that we can use to debug our server. So, we want to enable them to make our development process much easier.

Next, we define a GET route to the / endpoint. If you have worked with other frameworks such as Express.js, Flask, or Sinatra, you should have already familiar with this pattern.

To define a route, we need to specify two things, the endpoint, and handler. The endpoint is the path where the client wants to fetch. For instance, if the user wants to grab all books in our bookstore, they should fetch the /books endpoint. The handler, on the other hand, is to determine how we should provide the data to the client. This is where we put our business logic such as grabbing the data from the database, validating the user input, and so on.

There are several types of responses that we can send back to the client. But typically, RESTful APIs will give the response in JSON format. And to do such thing in Gin, we can use the JSON method provided from the request context. This method requires an HTTP status code and a JSON response as the parameters.

Lastly, we can run our server by simply invoke the Run method of our Gin instance.

To test it out, we can start our server by running the command below.

$ go run main.go

Setting up the database

The next thing we need to do is to build our database models.

Model is a class (or structs in Go) that allows us to communicate with a specific table in our database. In Gorm, we can create our models by defining a Go struct. This model will contain the properties that represent fields in our database table. Since we’re trying to build a bookstore API, let’s create a Book model.

models/Book.go
package models

import (
  "github.com/jinzhu/gorm"
)

type Book struct {
  ID     uint   `json:"id" gorm:"primary_key"`
  Title  string `json:"title"`
  Author string `json:"author"`
}

Our Book model is pretty straight forward. Each book should have a title and the author name which has a string data type, as well as an ID, which is a unique number to differentiate each book in our database.

We also specify the tags on each field using backtick annotation. This allows us to map each field into a different name when we send them as a response since JSON and Go has a different naming convention.

To organize our code a little bit, we can put this code inside a separate module called models.

Next, we need to create a utility function called ConnectDatabase which allows us to create a connection to the database and migrate our model’s schema. We can put this inside the setup.go file in our models module.

models/setup.go
package models

import (
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/sqlite"
)

var DB *gorm.DB

func ConnectDataBase() {
  database, err := gorm.Open("sqlite3", "test.db")

  if err != nil {
    panic("Failed to connect to database!")
  }

  database.AutoMigrate(&Book{})

  DB = database
}

Inside this function, we create a new connection with gorm.Open method. Here, we specify which kind of database we plan to use and how to access it. Currently, Gorm only supports four types of SQL databases. And for learning purposes, we’ll use SQlite and store our data inside test.db file. To connect our server to the database, we need to import the databases driver which located inside thegithub.com/jinzhu/gorm/dialects` module.

We also need to check whether the connection is created successfully. If it doesn’t, it will print out the error to the console and terminate the server.

Next, we migrate the database schema by using the AutoMigrate. Make sure to call this method on each model you have created.

Lastly, we populate the the DB variable with our database instance. We will use this variable in our controller in order to get access to our database.

In main.go, we need to call this function this function before we run our app.

main.go
package main

import (
  "net/http"
  "github.com/gin-gonic/gin"

  "github.com/rahmanfadhil/gin-bookstore/models" // new
)

func main() {
  r := gin.Default()

  models.ConnectDatabase() // new

  r.Run()
}

RESTful Routes

We are almost there!

The last thing we need to do is to implement our controllers. In the previous chapter, we have learned how we can create a route handler, i.e controller, inside our main.go file. However, this approach makes our code much harder to maintain. Instead of doing that, we can put our controllers inside a separate module called controllers.

Firstly, let’s implement controllers/books.go controller.

controllers/books.go
package controllers

import (
  "github.com/gin-gonic/gin"
  "github.com/rahmanfadhil/gin-bookstore/models"
)

// GET /books
// Get all books
func FindBooks(c *gin.Context) {
  var books []models.Book
  models.DB.Find(&books)

  c.JSON(http.StatusOK, gin.H{"data": books})
}

Here, we have a FindBooks function that will return all books from our database. In order to get access to our model and DB instance, we need to import our models module at the top.

Next, we can register our function as a route handler in main.go.

main.go
package main

import (
  "net/http"
  "github.com/gin-gonic/gin"

  "github.com/rahmanfadhil/gin-bookstore/models"
  "github.com/rahmanfadhil/gin-bookstore/controllers" // new
)

func main() {
  r := gin.Default()

  models.ConnectDatabase()

  r.GET("/books", controllers.FindBooks) // new

  r.Run()
}

Pretty simple right?

Make sure to add this line after the ConnectDatabase. Otherwise, your controller couldn’t access the database.

Now, let’s run your server and hit /books endpoint.

{
	"data": []
}

If you see an empty array as the result, it means your applications are working. The reason why we get this is that we haven’t created a book yet. To do so, let’s create a create book controller.

In order to create a book, we need to have a schema that can validate the users' input to prevent us from getting invalid data.

controllers/books.go
type CreateBookInput struct {
  Title  string `json:"title" binding:"required"`
  Author string `json:"author" binding:"required"`
}

The schema is very similar to our model. Instead, we don’t need the ID property since it will be generated automatically by the database.

Now, we can use that schema in our controller.

controllers/books.go
// POST /books
// Create new book
func CreateBook(c *gin.Context) {
  // Validate input
  var input CreateBookInput
  if err := c.ShouldBindJSON(&input); err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }

  // Create book
  book := models.Book{Title: input.Title, Author: input.Author}
  models.DB.Create(&book)

  c.JSON(http.StatusOK, gin.H{"data": book})
}

We first validate the request body by using the ShouldBindJSON method and pass the schema. If the data is invalid, it will return a 400 error to the client and tell them which fields are invalid. Otherwise, it will create a new book, save it to the database, and return the book.

Now, we can add the CreateBook controller in main.go.

main.go
func main() {
  // ...

  r.GET("/books", controllers.FindBooks)
  r.POST("/books", controllers.CreateBook) // new
}

So, if we try to send a POST request to /books endpoint with this request body:

{
	"title": "Start with Why",
	"author": "Simon Sinek"
}

The response should looks like this:

{
	"data": {
		"id": 1,
		"title": "Start with Why",
		"author": "Simon Sinek"
	}
}

Cool, now we have successfully created our first book. Let’s add controller that can fetch a single book.

controllers/books.go
// GET /books/:id
// Find a book
func FindBook(c *gin.Context) {  // Get model if exist
  var book models.Book

  if err := models.DB.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
    return
  }

  c.JSON(http.StatusOK, gin.H{"data": book})
}

Our FindBook controller is pretty similar with the FindBooks controller. Instead, we only get the first book that match the ID that we got from the request parameter. We also need to check wether the book is exist or not by simply wrap it inside an if statement.

Then, register it into your main.go.

main.go
func main() {
  // ...

  r.GET("/books", controllers.FindBooks)
  r.POST("/books", controllers.CreateBook)
  r.GET("/books/:id", controllers.FindBook) // new
}

In order to get the id parameter, we need to specify it from the route path as above.

Now, let’s run the server and fetch /books/1 to get the book that we have just created.

{
	"data": {
		"id": 1,
		"title": "Start with Why",
		"author": "Simon Sinek"
	}
}

So far so good. Now, let’s add the UpdateBook controller to update an existing book. But before we do that, let’s define the schema for validating the user input first.

controllers/books.go
struct UpdateBookInput {
  Title  string `json:"title"`
  Author string `json:"author"`
}

The UpdateBookInput schema is pretty much the same as our CreateBookInput. Instead, we don’t need to make those fields required since the user doesn’t have to fill all the properties of the book.

Now, let add the controller.

controllers/books.go
// PATCH /books/:id
// Update a book
func UpdateBook(c *gin.Context) {
  // Get model if exist
  var book models.Book
  if err := models.DB.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
    return
  }

  // Validate input
  var input UpdateBookInput
  if err := c.ShouldBindJSON(&input); err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }

  models.DB.Model(&book).Updates(input)

  c.JSON(http.StatusOK, gin.H{"data": book})
}

First, we can copy the code from the FindBook controller to grab a single book and make sure it exists. After we find the book, we need to validate the user input with the UpdateBookInput schema. Finally, we update the book model using the Updates method and return the updated book data to the client.

Register it into your main.go.

main.go
func main() {
  // ...

  r.GET("/books", controllers.FindBooks)
  r.POST("/books", controllers.CreateBook)
  r.GET("/books/:id", controllers.FindBook)
  r.PATCH("/books/:id", controllers.UpdateBook) // new
}

Let’s test it out! fire a PATCH request to /books/:id endpoint to update the book title.

{
	"title": "The Infinite Game"
}

And the result should be:

{
	"data": {
		"id": 1,
		"title": "The Infinite Game",
		"author": "Simon Sinek"
	}
}

Finally! The last step is to implement to delete book feature.

controllers/books.go
// DELETE /books/:id
// Delete a book
func DeleteBook(c *gin.Context) {
  // Get model if exist
  var book models.Book
  if err := models.DB.Where("id = ?", c.Param("id")).First(&book).Error; err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "Record not found!"})
    return
  }

  models.DB.Delete(&book)

  c.JSON(http.StatusOK, gin.H{"data": true})
}

Just like the update controller, we get the book model from the request parameters if it exists, and delete it with the Delete method from our database instance which we get from our middleware. Then, return true as the result since there is no reason to return a deleted book data back to the client.

main.go
func main() {
  // ...

  r.GET("/books", controllers.FindBooks)
  r.POST("/books", controllers.CreateBook)
  r.GET("/books/:id", controllers.FindBook)
  r.PATCH("/books/:id", controllers.UpdateBook)
  r.DELETE("/books/:id")
}

Let’s test it out by sending a DELETE request to /books/1 endpoint.

{
	"data": true
}

So, if we fetch all books in /books, we’ll see an empty array again.

{
	"data": []
}

Conclusion

Go has the best of both world features which most programming languages wish they have; simplicity and performance. Even though this technology might not be the best option for everyone, it is still a very solid solution and worth to learn.

By guiding you to build this project from scratch, I hope that you understand the basics concept of developing a RESTful API with Gin and Gorm, how they both work together, and how to implement the CRUD features. There is still plenty of room for improvements which I highly recommend you learn more about such as authenticate users with JWT, implement unit testing, containerize your app with Docker, and a lot of cool stuff out there.

Profile picture

Abdurrahman Fadhil

I'm a software engineer specialized in iOS and full-stack web development. If you have a project in mind, feel free to contact me and start the conversation.