DEV Community

Cover image for Blog with Go, Gin, MySQL and Docker - Part 2
Umesh Chaudhary
Umesh Chaudhary

Posted on • Originally published at nizu.tech

Blog with Go, Gin, MySQL and Docker - Part 2

Welcome to Part 2 of the Blog Project with Go, Gin, MySQL and Docker. Do make sure you have followed part 1, Here is the link.

Contents :

The respective repository can be found here. Clone the project Check out to branch part-2 by git checkout part-2 .

Architecture

We will be following Clean Architecture for this Blog Project. Clean architecture is art of writing software applications in a layered fashion. Please do read this article for more detailed information as all layers (repository, controller e.t.c ) are explained there. Following is the overview of the project that follows clean architecture and this is what you are going to follow. Isn't it awesome?

├── api │   ├── controller │   │   └── post.go │   ├── repository │   │   └── post.go │   ├── routes │   │   └── post.go │   └── service │   └── post.go ├── docker-compose.yml ├── Dockerfile ├── go.mod ├── go.sum ├── infrastructure │   ├── db.go │   ├── env.go │   └── routes.go ├── main ├── main.go ├── models │   └── post.go └── util └── response.go 
Enter fullscreen mode Exit fullscreen mode

Getting Started :

Designing Models

Create a folder models in project directory. Inside the models folder create a blog.go file and add following code

package models import "time" //Post Post Model type Post struct { ID int64 `gorm:"primary_key;auto_increment" json:"id"` Title string `gorm:"size:200" json:"title"` Body string `gorm:"size:3000" json:"body" ` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` } // TableName method sets table name for Post model func (post *Post) TableName() string { return "post" } //ResponseMap -> response map method of Post func (post *Post) ResponseMap() map[string]interface{} { resp := make(map[string]interface{}) resp["id"] = post.ID resp["title"] = post.Title resp["body"] = post.Body resp["created_at"] = post.CreatedAt resp["updated_at"] = post.UpdatedAt return resp } 
Enter fullscreen mode Exit fullscreen mode

We are defining Blog model which later gets converted into database table (gorm does this for us). TableName method sets a blog as a table name in the database for the Blog struct. ResponseMap is used to return response from Succesfull API calls. I assume you are familiar with Struct and methods in go.

Adding Repository Layer

This layer is the one that interacts and performs CRUD operations on the database.
Create a folder api on the project directory. Inside api folder create repository folder. Inside the repository folder create a blog.go file. The structure should look like this api -> repository -> blog.go. You can always refer to architecture section for project structure reference.

package repository import ( "blog/infrastructure" "blog/models" ) //PostRepository -> PostRepository type PostRepository struct { db infrastructure.Database } // NewPostRepository : fetching database func NewPostRepository(db infrastructure.Database) PostRepository { return PostRepository{ db: db, } } //Save -> Method for saving post to database func (p PostRepository) Save(post models.Post) error { return p.db.DB.Create(&post).Error } //FindAll -> Method for fetching all posts from database func (p PostRepository) FindAll(post models.Post, keyword string) (*[]models.Post, int64, error) { var posts []models.Post var totalRows int64 = 0 queryBuider := p.db.DB.Order("created_at desc").Model(&models.Post{}) // Search parameter if keyword != "" { queryKeyword := "%" + keyword + "%" queryBuider = queryBuider.Where( p.db.DB.Where("post.title LIKE ? ", queryKeyword)) } err := queryBuider. Where(post). Find(&posts). Count(&totalRows).Error return &posts, totalRows, err } //Update -> Method for updating Post func (p PostRepository) Update(post models.Post) error { return p.db.DB.Save(&post).Error } //Find -> Method for fetching post by id func (p PostRepository) Find(post models.Post) (models.Post, error) { var posts models.Post err := p.db.DB. Debug(). Model(&models.Post{}). Where(&post). Take(&posts).Error return posts, err } //Delete Deletes Post func (p PostRepository) Delete(post models.Post) error { return p.db.DB.Delete(&post).Error } 
Enter fullscreen mode Exit fullscreen mode

Let's explain above codes:

  • PostRepository : PostRepository struct has a db field which is a type of infrastructure.Database; which infact is a gorm database type. Infrastructure's Database part has been covered up in Part 1.
  • NewPostRepository : NewPostRepository takes database as argument and returns PostRepository. Database argument is provided while initializing the server on main.go file.
  • ** Save/FindAll/Find/Update/Delete ** : Perform CRUD operation to database using gorm ORM.

Adding Service Layer

This layer manages the communication between the inner and outer layers (Repository and Controller layers ). More detail here. Inside api folder create service folder. Inside the service folder create a blog.go file. The structure should look like this api -> service -> blog.go.Refer to architecture section for the structure.

package service import ( "blog/api/repository" "blog/models" ) //PostService PostService struct type PostService struct { repository repository.PostRepository } //NewPostService : returns the PostService struct instance func NewPostService(r repository.PostRepository) PostService { return PostService{ repository: r, } } //Save -> calls post repository save method func (p PostService) Save(post models.Post) error { return p.repository.Save(post) } //FindAll -> calls post repo find all method func (p PostService) FindAll(post models.Post, keyword string) (*[]models.Post, int64, error) { return p.repository.FindAll(post, keyword) } // Update -> calls postrepo update method func (p PostService) Update(post models.Post) error { return p.repository.Update(post) } // Delete -> calls post repo delete method func (p PostService) Delete(id int64) error { var post models.Post post.ID = id return p.repository.Delete(post) } // Find -> calls post repo find method func (p PostService) Find(post models.Post) (models.Post, error) { return p.repository.Find(post) } 
Enter fullscreen mode Exit fullscreen mode

Let's explain above codes:

  • PostService : PostService struct has repository field which is a type to PostRepository allowing access to PostRepository methods.
  • NewPostService : NewPostService takes PostRepository as argument and returns PostService allowing all PostRepository methods.
  • ** Save/FindAll/Find/Update/Delete ** : Calls respective repository methods.

Adding Controller Layer

This layer is used to grab the user input and process them or pass them to other layers. More about controllers here. But before adding code for the controller layer let's add some utilities which are used to return responses on sucessfull API calls.

Adding Utils

Create a util folder on project directory and a file response.go inside it. The structure should look like util -> response.go.

package util import "github.com/gin-gonic/gin" // Response struct type Response struct { Success bool `json:"success"` Message string `json:"message"` Data interface{} `json:"data"` } // ErrorJSON : json error response function func ErrorJSON(c *gin.Context, statusCode int, data interface{}) { c.JSON(statusCode, gin.H{"error": data}) } // SuccessJSON : json error response function func SuccessJSON(c *gin.Context, statusCode int, data interface{}) { c.JSON(statusCode, gin.H{"msg": data}) } 
Enter fullscreen mode Exit fullscreen mode
  • Response : Response is to return JSON Formatted success message with Struct data, here Blog data as of now.
  • ErrorJSON : ErrorJSON is used to return JSON Formatted error response
  • SuccessJSON : SuccessJSON is used to return JSON Formatted success message.

Create a controller folder inside api folder and blog.go file inside controller folder. Project structure folder should looks like api -> controller -> blog.go.

package controller import ( "blog/api/service" "blog/models" "blog/util" "net/http" "strconv" "github.com/gin-gonic/gin" ) //PostController -> PostController type PostController struct { service service.PostService } //NewPostController : NewPostController func NewPostController(s service.PostService) PostController { return PostController{ service: s, } } // GetPosts : GetPosts controller func (p PostController) GetPosts(ctx *gin.Context) { var posts models.Post keyword := ctx.Query("keyword") data, total, err := p.service.FindAll(posts, keyword) if err != nil { util.ErrorJSON(ctx, http.StatusBadRequest, "Failed to find questions") return } respArr := make([]map[string]interface{}, 0, 0) for _, n := range *data { resp := n.ResponseMap() respArr = append(respArr, resp) } ctx.JSON(http.StatusOK, &util.Response{ Success: true, Message: "Post result set", Data: map[string]interface{}{ "rows": respArr, "total_rows": total, }}) } // AddPost : AddPost controller func (p *PostController) AddPost(ctx *gin.Context) { var post models.Post ctx.ShouldBindJSON(&post) if post.Title == "" { util.ErrorJSON(ctx, http.StatusBadRequest, "Title is required") return } if post.Body == "" { util.ErrorJSON(ctx, http.StatusBadRequest, "Body is required") return } err := p.service.Save(post) if err != nil { util.ErrorJSON(ctx, http.StatusBadRequest, "Failed to create post") return } util.SuccessJSON(ctx, http.StatusCreated, "Successfully Created Post") } //GetPost : get post by id func (p *PostController) GetPost(c *gin.Context) { idParam := c.Param("id") id, err := strconv.ParseInt(idParam, 10, 64) //type conversion string to int64 if err != nil { util.ErrorJSON(c, http.StatusBadRequest, "id invalid") return } var post models.Post post.ID = id foundPost, err := p.service.Find(post) if err != nil { util.ErrorJSON(c, http.StatusBadRequest, "Error Finding Post") return } response := foundPost.ResponseMap() c.JSON(http.StatusOK, &util.Response{ Success: true, Message: "Result set of Post", Data: &response}) } //DeletePost : Deletes Post func (p *PostController) DeletePost(c *gin.Context) { idParam := c.Param("id") id, err := strconv.ParseInt(idParam, 10, 64) //type conversion string to uint64 if err != nil { util.ErrorJSON(c, http.StatusBadRequest, "id invalid") return } err = p.service.Delete(id) if err != nil { util.ErrorJSON(c, http.StatusBadRequest, "Failed to delete Post") return } response := &util.Response{ Success: true, Message: "Deleted Sucessfully"} c.JSON(http.StatusOK, response) } //UpdatePost : get update by id func (p PostController) UpdatePost(ctx *gin.Context) { idParam := ctx.Param("id") id, err := strconv.ParseInt(idParam, 10, 64) if err != nil { util.ErrorJSON(ctx, http.StatusBadRequest, "id invalid") return } var post models.Post post.ID = id postRecord, err := p.service.Find(post) if err != nil { util.ErrorJSON(ctx, http.StatusBadRequest, "Post with given id not found") return } ctx.ShouldBindJSON(&postRecord) if postRecord.Title == "" { util.ErrorJSON(ctx, http.StatusBadRequest, "Title is required") return } if postRecord.Body == "" { util.ErrorJSON(ctx, http.StatusBadRequest, "Body is required") return } if err := p.service.Update(postRecord); err != nil { util.ErrorJSON(ctx, http.StatusBadRequest, "Failed to store Post") return } response := postRecord.ResponseMap() ctx.JSON(http.StatusOK, &util.Response{ Success: true, Message: "Successfully Updated Post", Data: response, }) } 
Enter fullscreen mode Exit fullscreen mode

Let's explain above codes:

  • PostController : PostController struct has service field which is a type to PostService allowing access to PostService methods.
  • NewPostController : NewPostController takes PostService as argument and returns PostController allowing all PostController methods which are leveraged on controller.
  • ** GetPosts/AddPost/GetPost/DeletePost/UpdatePost ** : User Input are grabbed/ validated / processed / Service layers are called (which calls Repository methods; performing database operations) / and responses are returned by utility response functions.

Adding Routes

Till now we have created foundational part of the APIs. Let's create endpoints by adding routes.

package routes import ( "blog/api/controller" "blog/infrastructure" ) //PostRoute -> Route for question module type PostRoute struct { Controller controller.PostController Handler infrastructure.GinRouter } //NewPostRoute -> initializes new choice rouets func NewPostRoute( controller controller.PostController, handler infrastructure.GinRouter, ) PostRoute { return PostRoute{ Controller: controller, Handler: handler, } } //Setup -> setups new choice Routes func (p PostRoute) Setup() { post := p.Handler.Gin.Group("/posts") //Router group { post.GET("/", p.Controller.GetPosts) post.POST("/", p.Controller.AddPost) post.GET("/:id", p.Controller.GetPost) post.DELETE("/:id", p.Controller.DeletePost) post.PUT("/:id", p.Controller.UpdatePost) } } 
Enter fullscreen mode Exit fullscreen mode

Let's explain above codes:

  • PostRoute : PostRoute struct has Controller and Handler fields. Controller is a type of PostController and Handler is of type Gin Router. Gin Router here is used to create router group which is used later to create endpoint.s
    • NewPostRoute : NewPostRoute takes Controller and Handlre as arguments and returns PostRoute struct allowing access to PostController and Gin Router.
  • ** Setup ** : Setup method is used to configure endpoint for post APIs.

Main Router

Let's create a function to create and return Gin Router.
Create a routes.go file inside infrastructure folder. It should look like infrastructure -> routes.go.

package infrastructure import ( "net/http" "github.com/gin-gonic/gin" ) //GinRouter -> Gin Router type GinRouter struct { Gin *gin.Engine } //NewGinRouter all the routes are defined here func NewGinRouter() GinRouter { httpRouter := gin.Default() httpRouter.GET("/", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": "Up and Running..."}) }) return GinRouter{ Gin: httpRouter, } } 
Enter fullscreen mode Exit fullscreen mode

The above code configures and returns a Default Gin Router instance.

Gluing All Things Together

The foundational part has been now completed. The only part left is to glue things together. Edit main.go file with following code

package main import ( "blog/api/controller" "blog/api/repository" "blog/api/routes" "blog/api/service" "blog/infrastructure" "blog/models" ) func init() { infrastructure.LoadEnv() } func main() { router := infrastructure.NewGinRouter() //router has been initialized and configured db := infrastructure.NewDatabase() // databse has been initialized and configured postRepository := repository.NewPostRepository(db) // repository are being setup postService := service.NewPostService(postRepository) // service are being setup postController := controller.NewPostController(postService) // controller are being set up postRoute := routes.NewPostRoute(postController, router) // post routes are initialized postRoute.Setup() // post routes are being setup db.DB.AutoMigrate(&models.Post{}) // migrating Post model to datbase table router.Gin.Run(":8000") //server started on 8000 port } 
Enter fullscreen mode Exit fullscreen mode

That's all for the main.go.

Test APIs

It's time to spin the server and testing the APIs.
Fire up the server via Docker Compose via following command

docker-compose up --build 
Enter fullscreen mode Exit fullscreen mode

Now, Bring up your favorite API Tester application. I will be using Insomnia
Testing Create API endpoint -> /posts/
Post Create API Test
Testing Get All Post endpoint -> /posts/
Post Get All Test
Testing Get Post endpoint -> /posts/2
Post Get Test
Testing Update Post endpoint -> /posts/2
Post Update Test
Testing Delete Post endpoint -> /posts/2
Post Delete Test

Wrap Up

Link to Part 1 : Blog with Go, Gin, MySQL and Docker

Next Up
Upcoming Part 3 will cover the following

  • Adding User Struct & APIs
  • Related User Posts
  • Middleware Layer and many more..

That's a wrap. Hope you enjoyed the article. Do not hesitate to share your feedback. I am on Linkedin . Let's Connect !

Next Part will be published on next weekend. Please stay tuned.

Thank You for reading !

Top comments (2)

Collapse
 
jacobbishopxy profile image
Jacob Xie

Thanks for the great sharing! BTW, one thing I've noticed is that all the structs under api folder are using non-pointer fields. For example, why wouldn't we write PostRoute like this:

type PostRoute struct { Controller *controller.PostController Handler *infrastructure.GinRouter } 
Enter fullscreen mode Exit fullscreen mode

And as a result, we can do main.go like below (using references):

func main() { router := infrastructure.NewGinRouter() db := infrastructure.NewDatabase() postRepository := repository.NewPostRepository(&db) postService := service.NewPostService(&postRepository) postController := controller.NewPostController(&postService) postRoute := routes.NewPostRoute(&postController, &router) postRoute.Setup() db.DB.AutoMigrate(&models.Post{}) router.Gin.Run(":8080") } 
Enter fullscreen mode Exit fullscreen mode

Again, thanks for sharing!

Collapse
 
tiburonin91 profile image
Isaac Contreras

Hi Umesh,

I'm attempting to go through part 2 here of your tutorial, but I keep running into a certain error and was hoping you could help. I get this when running docker-compose up --build:

web-1 | 2024/12/13 00:35:02 stdout: 2024/12/13 00:35:02 /app/infrastructure/db.go:28
web-1 | 2024/12/13 00:35:02 stdout: [error] failed to initialize database, got error dial tcp 192.168.65.254:3306: connect: connection refused
web-1 | 2024/12/13 00:35:02 stderr: panic: Failed to connect to database!

I'm running on Apple silicon and tried changing DB_HOST in .env to host.docker.internal and also docker.for.mac.localhost but I get the same error, just at different IPs. Hope you can get back to me, the tutorial has been very helpful so far!