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.
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:
$ 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
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
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()
}
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": []
}
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.
#golang #go #webdev