Rahman Fadhil I'm a full-stack developer, instructor, speaker, content creator, and eternal learner from Indonesia. I wrote my first "Hello world" app when I was 10 and I love to help people reach their dreams through programming.

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

9 min read 2747

How to Build a REST API With Golang Using Gin and Gorm

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 it’s also incredibly simple, which makes the development experience delightful.

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 may or may not be the best option for your project, depending on the scenario.

Gin is a microframework 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. Another web framework, such as Laravel and Ruby on Rails, might be more appropriate for such a team. Such 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. But 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:

We made a custom demo for .
No really. Click here to check it out.

  • 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 of these files contain information about the packages you installed, which is helpful when working with other developers. If somebody wants to contribute to the project, all they need to do is run the go mod download command on their terminal to install all the required dependencies 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 the server

Let’s start by creating a Hello World server inside the main.go file.

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’ll initialize a new Gin router within the r variable. We’re using the Default router because Gin provides some useful middlewares we can use to debug our server.

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

To define a route, we need to specify two things: the endpoint and the handler. The endpoint is the path the client wants to fetch. For instance, if the user wants to grab all books in our bookstore, they’d fetch the /books endpoint. The handler, on the other hand, determines how we 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.

We can send several types of response to the client, but RESTful APIs typically give the response in JSON format. To do that 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 invoking the Run method of our Gin instance.

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

$ go run main.go

Setting up the database

The next step is to build our database models.

A model is a class (or a struct in Go) that enables us to communicate with a specific table in our database. In Gorm, we can create our model 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 straightforward; each book has a title and an author name, which has a string data type and a unique ID number to differentiate each book in the database.

We’ll 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 have different naming conventions.

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

Now let’s create a utility function called SetupModels, which allows us to create a connection with our 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"
)

func SetupModels() *gorm.DB {
  db, err := gorm.Open("sqlite3", "test.db")

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

  db.AutoMigrate(&Book{})

  return db
}

Inside this function, we’ll create a new connection with gorm.Open method, where we’ll specify what kind of database we plan to use and how to access it. Currently, Gorm only supports four types of SQL databases. For our purposes, we’ll use SQLite and store our data inside the test.db file.

To connect our server to the database, we need to import the database’s driver, which is located inside the github.com/jinzhu/gorm/dialects module.

We also need to check whether the connection was created successfully. If not, it will print out the error to the console and terminate the server.

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

We can call this function in our main.go file.

package main

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

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

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

  db := models.SetupModels() // new

  r.Run()
}

RESTful Routes

We’re almost there!

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

But before we do that, we need to create a middleware that can provide the database instance to every single controller since they live in another file that can’t access the database instance directly.

// main.go

// ...

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

  db := models.SetupModels()

  // Provide db variable to controllers
  r.Use(func(c *gin.Context) {
    c.Set("db", db)
    c.Next()
  })

  r.Run()
}

Middleware is basically a function that interferes with the client’s request. There are myriad benefits of using middleware. For instance, it enables you to provide data to the next route handler whenever a request comes in.

In this case, we are providing the db variable by using the Set method from the context. Be sure to call the next handler using the Next method. Otherwise, your API will do nothing when you send a request.

// 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) {
  db := c.MustGet("db").(*gorm.DB)

  var books []models.Book
  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 the first line of our function, we’re trying to get the database instance we provide in our middleware so that we can use it to fetch our books and return it to the client. We also need to import our models module at the top so we can reference our Book model in each controller.

Next, we’ll register our function as a route handler in 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()

  db := models.SetupModels()

  r.Use(func(c *gin.Context) {
    c.Set("db", db)
    c.Next()
  })

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

  r.Run()
}

Pretty simple, right?

Make sure you add this line before the database middleware. Otherwise, your controller won’t be able to access your database instance.

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

{
  "data": []
}

If an empty array shows as the result, it means your applications are working. We get this result because we haven’t created a book yet. Let’s create a create book controller.

To create a book, we need to have a schema that can validate the user’s input to prevent us from getting invalid data.

type CreateBookInput struct {
  Title  string `json:"title" binding:"required"`
  Author string `json:"author" binding:"required"`
}

The schema is very similar to our model. We don’t need the ID property since the database will generate it automatically.

Now we can use that schema in our controller.

// POST /books
// Create new book
func CreateBook(c *gin.Context) {
  db := c.MustGet("db").(*gorm.DB)

  // 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.Author, Author: input.Author}
  db.Create(&book)

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

We can now validate the request body 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 it.

Now it’s time to add the CreateBook controller in main.go.

func main() {
  // ...

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

Let’s send a POST request to the /books endpoint with the following request body.

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


The response should look like this:
{
  "data": {
    "id": 1,
    "title": "Start with Why",
    "author": "Simon Sinek"
  }
}

Now that we’ve successfully created our first book, let’s add a controller that can fetch a single book.

// GET /books/:id
// Find a book
func FindBook(c *gin.Context) {
  db := c.MustGet("db").(*gorm.DB)

  // Get model if exist
  var book models.Book
  if err := 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 to the FindBooks controller, except that we only get the first book that matches the ID we got from the request parameter. We also need to check wether the book exists by simply wrapping it inside an if statement.

Next, register it into your main.go file.

func main() {
  // ...

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

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

Now let’s run the server and fetch /books/1 to get the book we 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, we must define the schema for validating the user input first.

struct UpdateBookInput {
  Title  string `json:"title"`
  Author string `json:"author"`  
}

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

Use the following code to add the controller.

// PATCH /books/:id
// Update a book
func UpdateBook(c *gin.Context) {
  db := c.MustGet("db").(*gorm.DB)

  // Get model if exist
  var book models.Book
  if err := 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
  }

  db.Model(&book).Updates(input)

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

First, we’ll 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’ll update the book model using the Updates method and return the updated book data to the client.

Register it into your 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! Fire a PATCH request to /books/:id endpoint to update the book title.

{
  "title": "The Infinite Game"
}

The result should be:

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

The last step is to implement the DeleteBook feature.

// DELETE /books/:id
// Delete a book
func DeleteBook(c *gin.Context) {
  db := c.MustGet("db").(*gorm.DB)

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

  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 back to the client.

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 the /books/1 endpoint.

{
  "data": true
}

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

{
  "data": []
}

Conclusion

Go offers two major qualities that all developers desire and all programming languages aim to achieve: simplicity and performance. While this technology may not be the best option for every developer team, it’s still a very solid solution and a skill worth learning.

By building this project from scratch, I hope you gained a basic understanding of how to develop a RESTful API with Gin and Gorm, how they work together, and how to implement the CRUD features. There is still plenty of room for improvement, such as authenticating users with JWT, implementing unit testing, containerizing your app with Docker, and a lot of other cool stuff you can mess around with if you want to dig deeper.

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Rahman Fadhil I'm a full-stack developer, instructor, speaker, content creator, and eternal learner from Indonesia. I wrote my first "Hello world" app when I was 10 and I love to help people reach their dreams through programming.

Leave a Reply