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
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
}
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
}
Let's explain above codes:
-
PostRepository : PostRepository struct has a
db
field which is a type ofinfrastructure.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 onmain.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)
}
Let's explain above codes:
-
PostService : PostService struct has
repository
field which is a type to PostRepository allowing access toPostRepository
methods. -
NewPostService : NewPostService takes
PostRepository
as argument and returnsPostService
allowing allPostRepository
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})
}
-
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,
})
}
Let's explain above codes:
-
PostController : PostController struct has
service
field which is a type to PostService allowing access toPostService
methods. -
NewPostController : NewPostController takes
PostService
as argument and returnsPostController
allowing allPostController
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)
}
}
Let's explain above codes:
-
PostRoute : PostRoute struct has
Controller
andHandler
fields.Controller
is a type ofPostController
andHandler
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
andHandlre
as arguments and returnsPostRoute
struct allowing access toPostController
andGin Router
.
-
NewPostRoute : NewPostRoute takes
- ** 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,
}
}
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
}
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
Now, Bring up your favorite API Tester application. I will be using Insomnia
Testing Create API endpoint -> /posts/
Testing Get All Post endpoint -> /posts/
Testing Get Post endpoint -> /posts/2
Testing Update Post endpoint -> /posts/2
Testing Delete Post endpoint -> /posts/2
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)
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:And as a result, we can do
main.go
like below (using references):Again, thanks for sharing!
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 alsodocker.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!