Simple RESTful API With Golang using Gorm and Gin.

sabin shrestha
readytowork, Inc.
Published in
12 min readMar 30, 2023

Introduction to Go, Gin, and Gorm:

Go, also known as Golang, is a statically typed, compiled high-level programming language that was designed by Google. People often like to compare golang with other “low-level” programming languages such as java and C++. It is a popular programming language for a good reason as it can be used for programming scalable servers and large software systems and many more. Features like simplicity, strong typing, and efficient memory management make it a popular choice for web development.

Gin is a Go web framework used for building high-performance RESTful APIs. It is based on the Martini framework and is simple, flexible, and fast. Gin can navigate API routes faster than Martini and supports middleware, making it popular among developers.

Gorm on the other hand, is a Go Object-Relational Mapping (ORM) library that simplifies database interactions such as creating, reading, updating, and deleting records. It supports multiple databases including MySQL, PostgreSQL, and SQLite, and provides a unified interface for working with them.

So, in this article, I will walk you through the process of building a simple blog RESTful API that provides blog data and performs CRUD operations using Golang, Gin, and Gorm. I’ll cover the basics of each of these tools and then demonstrate how to use them together to build a functional API that can perform basic HTTP operations (GET, POST, PATCH/PUT, DELETE) on data stored in a database. This article is a reference to those from beginners to intermediate Go developers who want to build RESTful APIs and will provide a solid foundation for building more complex APIs in the future. By the end of this article, you will have a good understanding of how to create a simple REST API using Go, Gin, and Gorm.

Setting up the server

Before you start building a RESTful API using Go, Gin, and Gorm, you need to install Go, create a project structure and install the Gin and Gorm libraries.

To install Go, you have to download the latest version from Go’s official website and follow the installation instructions for your operating system. You can also use a package manager if you’re on a Linux or MacOS system.

Once you have installed Go, you can create a new project by creating a directory for your code and initializing a Go module by running the given command:

go mod init go-gin

I have given go-gin a module name. However, you can give any name you want.

You can then install the gin and gorm library by running the following command in your Go environment folder.

go get github.com/gin-gonic/gin gorm.io/gorm

After installation you will see two folders, go.mod and go.sum. go.mod is similar to the package.json if you have come from the node. Both files contain information regarding packages that are installed for the project. It is helpful if you are working in the team, another developer can simply run go mode download command to install all the required packages for the project.

So, with this, you have everything you require to build your blog RESTful API. Let's create our main.go file and code our simple “ping pong” server.

package main

import "github.com/gin-gonic/gin"

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}

// path: /main.go

Here inside the main function we have created gin’s default router which is assigned to a variable r. Then we have defined a GET route to the /ping endpoint.

And at the end, we are running the server by invoking Run the method. This pattern is similar to other frameworks such as express.js, flask etc. [note: by default the server runs on port 8080].

To run the server run the command below:

go run main.go

To check if the server is running or not let’s send a GET request to http://localhost:8080/ping endpoint you should get this as a response:

{"message":"pong"}

As we can see to create a route, the endpoint and handler must be defined. The endpoint is the path the client wants to access and the handler is where the business logic resides and determines how the data is provided to the client as a response.

Model definition and database setup:

The next step is to define your data model and set up a database. To define your data model, you’ll need to create a Go struct that represents the fields you want to store in your database table. In this case, you’re building an API for a blog hence let's create Post model inside a separate models folder.

package models

import "gorm.io/gorm"

type Post struct {
gorm.Model
Title string
Body string
}

// path: /models/postModel.go

Our POST model we have created here is pretty straightforward.

  • gorm.Model: This field is a special field provided by the GORM library and includes the common columns for any database table like ID, CreatedAt, UpdatedAt, and DeletedAt.
  • Title: A string field that represents the title of the post.
  • Body: A string field that represents the body content of the post.

Once you have defined your data model, you can set up a database and connect it to your API using Gorm. You can use any database that Gorm supports, such as MySQL, PostgreSQL, or SQLite. I’m using PostgreSQL but you can use whatever you prefer. To connect to a database, you’ll need to create a Gorm instance and configure it with the connection information for your database.

package initializers

import (
"os"

"gorm.io/driver/postgres"
"gorm.io/gorm"
)

var DB *gorm.DB

func ConnectToDB() {
var err error;
dsn := "host=localhost user=postgres password=3055 dbname=golang port=5432 sslmode=disable"
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})

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

// path: /initializers/database.go

This is the code that connects PostgreSQL Database you can look up to gorm official website to connect to your preferred database.

[ NOTE: You are required to install your preferred database driver like in this case, I had to install PostgreSQL driver beforehand. To install PostgreSQL driver simply run this command:

go get gorm.io/driver/postgres

and you are good to go. ]

Let me explain what is going on in the file database.go:

  • DB: A global variable of a type gorm.DB that represents the database connection.
  • ConnectToDB: A function that connects to the database using a hard coded connection string. The connection is established using the postgres driver from the GORM library.

The ConnectToDB the function opens a connection to the database using the gorm.Open function and the postgres.Open function. The connection string includes the host, username, password, database name, port, and SSL mode for the PostgreSQL server. If the connection fails, the function will panic with an error message.

If the connection is successful, the DB variable will store the database connection and can be used throughout the application to interact with the database.

Now we have to migrate our database to create Post a table for us. Let's create a new file called migrate.go inside a new folder called migrate.

package main

import (
"go-gin/initializers"
"go-gin/models"
)

func init(){
initializers.ConnectToDB()
}

func main() {
// Migrate the schema
initializers.DB.AutoMigrate(&models.Post{})
}

// path: /migrate/migrate.go

Let me explain the above code:

  • The init function: This function is called automatically when the program starts and calls the ConnectToDB function from the initializers package to establish a connection to the database.
  • The main function: This function is the entry point of the program and performs database migrations using the AutoMigrate method from the GORM library. We are using DB that represents the Database connection from earlier. The AutoMigrate the method takes a pointer to a Go struct that represents a database table and creates the table if it does not exist. In our case, the models.Post(Same Model that we have already created in the post.go file).

Together, these two functions ensure that a database connection is established and that the Post table is created in the database. Run this separately and you should have Post a table created in your database. To run this file execute this command:

go run migrate/migrate.go

Creating endpoints with Gin:

Let’s implement our controllers. Previously, we created a server inside a main.go file and we created a route handler/controller in the same file. But, that doesn’t look clean at all since everything is dumped inside. Once we get to more complex logic handling, believe me, it's going to look ugly and hard to read.

That’s why you wanna put all your Post controllers inside a separate folder called controllers instead in a file named postController.go.

POST Handler function to create a new post

To create a Post, we need to have a schema that can validate the user’s input to prevent us from getting invalid data. It starts by defining an anonymous struct type with two fields, “Title” and “Body”, both of type string. The binding:"required" tag indicates that the field is required and must have a non-empty value.

var body struct{
Title string `json:"title" binding:"required"`
Body string `json:"body" binding:"required"`
}

Then we use the Bind method on the context to parse the request body into the anonymous struct. If the request body doesn’t contain the required “title” and “body” keys, the binding will fail.

package controllers

import (
"go-gin/initializers"
"go-gin/models"
"net/http"

"github.com/gin-gonic/gin"
)

func PostsCreate(ctx *gin.Context){
// To create a Post, we need to have a schema that
// can validate the user’s input to prevent us from getting invalid data:

var body struct{
Title string `json:"title" binding:"required"`
Body string `json:"body" binding:"required"`
}
ctx.Bind(&body)
// Get data off request body create a post and return it.
post := models.Post{Title: body.Title, Body: body.Body}
result := initializers.DB.Create(&post) // pass pointer of data to Create

if(result.Error != nil){
ctx.JSON(http.StatusInternalServerError, gin.H{"message": "Error creating post"})
return
}
ctx.JSON(http.StatusOK, gin.H{"message": "Post created successfully", "post": post})

}

// path: /controllers/postsController.go

The function creates a “models.Post” struct with the values from the anonymous struct and passes a pointer to this struct to the Create method on the "initializers.DB" value. This is a database instance that implements a Create method to insert a new record into the database.

An error is checked while creating a new post, if found the function responds with a JSON object with an error message. If there’s no error, the handler function responds with a JSON object with a success message and a newly created post along with a status code of 200 OK.

Now, we can add the PostsCreate controller in main.go:

package main

import (
"go-gin/controllers" // recently added
"go-gin/initializers" // recenlty added

"github.com/gin-gonic/gin"
)

func init(){
initializers.ConnectToDB(); // recently added
}

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

r.POST("/posts", controllers.PostsCreate); // recently added

r.Run();
}

// path: /main.go

As you can see in main.go file, I have defined an init function that connects to a database by calling the ConnectToDB function from the "initializers" package.

Let’s send a POST request to http://localhost:8080/posts the endpoint with this request body:

I’m using postman here.

The response should look something like this.

GET Handler function to retrieve a list of blog posts.

//...

func PostsFindAll(ctx *gin.Context){
var posts []models.Post
initializers.DB.Find(&posts)
ctx.JSON(http.StatusOK, gin.H{"message": "All posts", "posts": posts})
}

// path: /controllers/postsController.go

Here we have put PostsFindAll a function that returns all the stored blog posts from the database.

Findmethod on the database object initializers.DB retrieves all Post records from the database and returns them as a response along with a success message along with a status code of 200 OK.

Once you add this function in main.go the file and make GET request to /posts route you can retrieve all stored posts from the database.

// ...

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

r.POST("/posts", controllers.PostsCreate)
r.GET("/posts", controllers.PostsFindAll) // recently added

r.Run()
}

// path: /main.go

Now, let’s run our server and make GET request to our http://localhost:8080/posts endpoint in the postman.

If you get your recently added post as a response, that means your server is working. Here I have more than one post that's because I added them.

GET The handler function retrieves a single post.

// ...
func PostFindOne(ctx *gin.Context){
var post models.Post
id := ctx.Param("id")

initializers.DB.First(&post, id)
ctx.JSON(http.StatusOK, gin.H{"message": "Post found", "post": post})
}

// path: /controllers/postsController.go

Here is PostsFindOne a function where we have used Firstmethod on the database object initializers.DB that retrieves the first post that matches the ID that we got from the request parameter from the database and finally, returns it as a response with a success message and status code of 200 OK.

Next, register it into your main.go


// ...

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

r.POST("/posts", controllers.PostsCreate)
r.GET("/posts", controllers.PostsFindAll)
r.GET("/posts/:id", controllers.PostFindOne) // recently added

r.Run()
}

// path: /main.go

Now, let’s run our server and make GET request to our http://localhost:8080/posts/10 endpoint in the postman to retrieve the post we just created, in my case it has an id of 10.

UPDATE the handler function to edit an existing post.

Congratulation, you are doing really great so far, but wait what if we want to update our existing post. For example, what if I want to change the title of the post that we just created. For that let’s add the PostUpdate function to update an existing Post.

Similar to the PostsCreate let's define a struct, but this time we don’t need to make those fields required as the user might or might not want to edit every field.

    var input struct {
Title string `json:"title"`
Body string `json:"body"`
}

Now like in PostCreate function we use ctx.Bind(&body)to bind the JSON payload to the input struct.

// ...

func PostUpdate(ctx *gin.Context) {
id := ctx.Param("id")

var input struct {
Title string `json:"title"`
Body string `json:"body"`
}

ctx.Bind(&input)

var post models.Post
if err := initializers.DB.First(&post, id).Error; err != nil {
ctx.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
return
}

if err := initializers.DB.Model(&post).Updates(&models.Post{Title: input.Title, Body: input.Body}).Error; err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update post"})
return
}

ctx.JSON(http.StatusOK, gin.H{"message": "Post updated", "post": post})
}

// path: /controllers/postsController.go

We need an id to edit the specific post that’s why we retrieve the **id**of the post from the request URL parameter using the **Param**method on the context object.

Next, we check if there is any post that matches with the id if nothing is returned then it responds with an error message. If the post is found, the function updates the Title and Body fields of the retrieved post object with the values from the input struct using the Updates method. If the update operation fails, the function returns a JSON response with a 500 status code and an error message. If it doesn’t then the function returns a JSON response with a 200 status code, a message indicating that the post was updated, and the updated post object.

Let’s register it into your main.go:

// ...

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

r.POST("/posts", controllers.PostsCreate)
r.GET("/posts", controllers.PostsFindAll)
r.GET("/posts/:id", controllers.PostFindOne)
r.PATCH("/post/:id", controllers.PostUpdate) // recently added

r.Run()
}

// path: /main.go

Let’s send a PATCH request to http://localhost:8080/posts/10 endpoint with this request body:

We are only updating the title of our recently created blog post.

The response should look something like this.

DELETE the handler function to delete an existing post.

We are almost there, the last thing to implement is PostDelete function, it's pretty simple.

//..

func PostDelete(ctx *gin.Context){
id := ctx.Param("id")

if(initializers.DB.First(&models.Post{}, id).RowsAffected == 0){
ctx.JSON(404, gin.H{"message": "Post not found"})
return
}

initializers.DB.Delete(&models.Post{}, id)

ctx.JSON(200, gin.H{"message": "Post deleted successfully"})
}

// path: /controllers/postsController.go

The approach is not different than what we have been doing till now. We retrieve the id from the parameter and use it to check if the post exists, if it does then, use Delete the method to delete that particular post from the database. Once the post has been deleted the function returns a JSON response with a 200 status code and a success message.

Now, we can add the PostDelete controller in main.go:

// ...

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

r.POST("/posts", controllers.PostsCreate)
r.GET("/posts", controllers.PostsFindAll)
r.GET("/posts/:id", controllers.PostFindOne)
r.PATCH("/post/:id", controllers.PostUpdate)
r.DELETE("/post/:id", controllers.PostDelete) // recently added

r.Run()
}

// path: /main.go

Let’s send a DELETE request to http://localhost:8080/posts/10 endpoint.

As you can see we have successfully deleted our blog post from the database.

Conclusion:

To conclude, this article introduces the programming language Go and its usefulness in web development, particularly in building scalable servers and large software systems. The article then discusses two tools, Gin and Gorm, that are commonly used with Go to build RESTful APIs and interact with databases.

Further, I have demonstrated how to use Go, Gin, and Gorm to build a simple blog RESTful API that performs CRUD operations on data stored in a PostgreSQL database. This article is a great resource for beginners and intermediate Go developers who want to learn how to build RESTful APIs using these tools. This is a very simple thing that we have accomplished with golang, there is plenty of space for improvements and things that we can implement in this, like JWT authentication, containerizing your app with Docker, and a lot more. If you want then you can do your own research and play around with this app and dive deeper.

--

--