Blog CRUD APIs with Clean Architecture

In this Chapter we will create CRUD APIs for blog post following Clean Architecture of writing software projects.

Published on

19 min read

Backend As A Service with Go, Gin, Mysql & Docker

Chapters
Feature Image

Welcome to the Chapter-3 of Series:Backend as a service Go, Gin, MySQL and Docker. We left Chapter-2 by configuring our application with Docker & MySQL. Let's continue our journey.

Architecture

We will be following Clean Architecture for to write our APIs. 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. The tree layout of the project structure is given below. Isn't it awesome?

├── api
│   ├── controllers
│   │   └── blog.go
│   ├── repositories
│   │   └── blog.go
│   ├── routes
│   │   └── blog.go
│   └── services
│       └── blog.go
├── docker-compose.yml
├── Dockerfile
├── go.mod
├── go.sum
├── infrastructure
│   ├── db.go
│   └── routes.go
├── main.go
├── models
│   └── blog.go
└── utils
    └── 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 Post 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 Post 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 repositories folder. Inside the repositories folder create a blog.go file. The structure should look like this api/repositories/blog.go. You can always refer to architecture section for project structure reference.

package repositories
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 of infrastructure.Database; which infact is a gorm database type. This Database part has been covered up Here in Chapter-2.
  • 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 ). Inside api folder create services folder and file blog.go inside it. The structure should look like this api/services/blog.go. Refer to architecture section for the structure.

package services

import (
	"blog/api/repositories"
	"blog/models"
)

//PostService PostService struct
type PostService struct {
	repositories repositories.PostRepository
}

//NewPostService : returns the PostService struct instance
func NewPostService(r repositories.PostRepository) PostService {
	return PostService{
		repositories: r,
	}
}

//Save -> calls post repository save method
func (p PostService) Save(post models.Post) error {
	return p.repositories.Save(post)
}

//FindAll -> calls post repo find all method
func (p PostService) FindAll(post models.Post, keyword string) (*[]models.Post, int64, error) {
	return p.repositories.FindAll(post, keyword)
}

// Update -> calls postrepo update method
func (p PostService) Update(post models.Post) error {
	return p.repositories.Update(post)
}

// Delete -> calls post repo delete method
func (p PostService) Delete(id int64) error {
	var post models.Post
	post.ID = id
	return p.repositories.Delete(post)
}

// Find -> calls post repo find method
func (p PostService) Find(post models.Post) (models.Post, error) {
	return p.repositories.Find(post)
}

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 grabs the user input and process them or pass them to other layers. Before adding code for the controller layer let's add some utilities which are used to return responses on sucessfull/unsuccessfull API calls.

Adding Utils

Create a utils folder on project directory and a file response.go inside it. The structure should look like utils/response.go.

package utils

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 controllers folder inside api folder and blog.go file inside controllers folder. Project structure should looks like api/controllers/blog.go.

package controllers

import (
	"blog/api/services"
	"blog/models"
	"blog/utils"
	"net/http"
	"strconv"

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

//PostController -> PostController
type PostController struct {
	services services.PostService
}

//NewPostController : NewPostController
func NewPostController(s services.PostService) PostController {
	return PostController{
		services: s,
	}
}

// GetPosts : GetPosts controller
func (p PostController) GetPosts(ctx *gin.Context) {
	var posts models.Post

	keyword := ctx.Query("keyword")

	data, total, err := p.services.FindAll(posts, keyword)

	if err != nil {
		utils.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, &utils.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 == "" {
		utils.ErrorJSON(ctx, http.StatusBadRequest, "Title is required")
		return
	}
	if post.Body == "" {
		utils.ErrorJSON(ctx, http.StatusBadRequest, "Body is required")
		return
	}
	err := p.services.Save(post)
	if err != nil {
		utils.ErrorJSON(ctx, http.StatusBadRequest, "Failed to create post")
		return
	}
	utils.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 {
		utils.ErrorJSON(c, http.StatusBadRequest, "id invalid")
		return
	}
	var post models.Post
	post.ID = id
	foundPost, err := p.services.Find(post)
	if err != nil {
		utils.ErrorJSON(c, http.StatusBadRequest, "Error Finding Post")
		return
	}
    response := foundPost.ResponseMap()

	c.JSON(http.StatusOK, &utils.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 {
		utils.ErrorJSON(c, http.StatusBadRequest, "id invalid")
		return
	}
	err = p.services.Delete(id)

	if err != nil {
		utils.ErrorJSON(c, http.StatusBadRequest, "Failed to delete Post")
		return
	}
	response := &utils.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 {
		utils.ErrorJSON(ctx, http.StatusBadRequest, "id invalid")
		return
	}
	var post models.Post
	post.ID = id

	postRecord, err := p.services.Find(post)

	if err != nil {
		utils.ErrorJSON(ctx, http.StatusBadRequest, "Post with given id not found")
		return
	}
	ctx.ShouldBindJSON(&postRecord)

	if postRecord.Title == "" {
		utils.ErrorJSON(ctx, http.StatusBadRequest, "Title is required")
		return
	}
	if postRecord.Body == "" {
		utils.ErrorJSON(ctx, http.StatusBadRequest, "Body is required")
		return
	}

	if err := p.services.Update(postRecord); err != nil {
		utils.ErrorJSON(ctx, http.StatusBadRequest, "Failed to store Post")
		return
	}
	response := postRecord.ResponseMap()

	ctx.JSON(http.StatusOK, &utils.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 to PostService methods.
  • NewPostController : NewPostController takes PostService as argument and returns PostController allowing all PostController methods which are leveraged on controller.
  • Get/Add/Delete/Update : 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. Now let's configure the routes. Create a routes folder inside api folder and blog.go file inside routes folder. Project structure should looks like api/routes/blog.go. Let's create endpoints by adding routes.

package routes

import (
	"blog/api/controllers"
	"blog/infrastructure"
)

//PostRoute -> Route for question module
type PostRoute struct {
	Controller controllers.PostController
	Handler    infrastructure.GinRouter
}

//NewPostRoute -> initializes new choice rouets
func NewPostRoute(
	controller controllers.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 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,
	}
}

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/controllers"
	"blog/api/repositories"
	"blog/api/routes"
	"blog/api/services"
	"blog/infrastructure"
	"blog/models"
)

func main() {

	router := infrastructure.NewGinRouter() //router has been initialized and configured
	db := infrastructure.NewDatabase() // databse has been initialized and configured
	postRepository := repositories.NewPostRepository(db) // repository are being setup
	postService := services.NewPostService(postRepository) // service are being setup
	postController := controllers.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 with the following command

docker-compose up --build

You should see similar outpt

web_1 | Database connection established
web_1 | [GIN-debug] GET    /posts/                   --> blog/api/controllers.PostController.GetPosts-fm (3 handlers)
web_1 | [GIN-debug] POST   /posts/                   --> blog/api/controllers.(*PostController).AddPost-fm (3 handlers)
web_1 | [GIN-debug] GET    /posts/:id                --> blog/api/controllers.(*PostController).GetPost-fm (3 handlers)
web_1 | [GIN-debug] DELETE /posts/:id                --> blog/api/controllers.(*PostController).DeletePost-fm (3 handlers)
web_1 | [GIN-debug] PUT    /posts/:id                --> blog/api/controllers.PostController.UpdatePost-fm (3 handlers)
web_1 | [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
web_1 | Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
web_1 | [GIN-debug] Listening and serving HTTP on :8000

Now, Bring up your favorite API clien application. I will be using Insomnia

Testing Create API endpoint -> /posts/

Testing Get All Post endpoint -> /posts/

Testing Get Post endpoint -> /posts/1

Testing Update Post endpoint -> /posts/1

Testing Delete Post endpoint -> /posts/1

Here is the Repo that contains all of the above codes.

That's a Wrap

Congratulations on completing this chapter. We are really making some great progress here.

Medals for you 🥇🥇🥇

Next Up In the next chapter we will be working on

  • User struct
  • User registration and login apis
  • Jwt tokens

Hope you enjoyed this chapter !
Do not hesitate to share your feedback.
I am on Linkedin. Let's Connect!

Thank You for reading!
Happy Learning!