DEV Community

Cover image for Golang REST API with Domain Driven Design
Ilham S
Ilham S

Posted on • Edited on

6 2 1 1

Golang REST API with Domain Driven Design

Food App Backend Golang with Domain Driven Design
...

Please kindly visit this repo for the complete source code :Link

List of libraries we will be using :

  • Viper Link
  • Gin Web Framework Link
  • Go ORM / GORM Link
  • Go JWT Implementation Link
  • Go MySQL driver Link
  • Go Email validation Link
  • Bcrpyt hashing Link

...

First thing first

Let's start by initiating dependency management in Go using go.mod, execute in root project

Path: food-app/

go mod init food-app

....

Folder Organization

Alt Text
...

Pkg layer

Environment config

We use MySQL database, the connection configuration will be written in file App.yaml folder configurations

App:
Port: 8081
Database:
Host: "127.00.0.1"
Port: 3306
Username: "root"
Password: "p455w0rd"
DBName: "food-app"
Files:
Path: "./files"
Jwt:
Secret: "q1w2e3r4t5y6"
view raw App.yaml hosted with ❤ by GitHub

Please adjust the config to fit your local database

Then, inside pkg folder , we make package config to read the configuration from App.yaml . We use viper library to read local config file App.yaml

Path: food-app/pkg/config

package config
import (
"log"
"github.com/spf13/viper"
)
func GetConfig() {
viper.SetConfigName("App")
viper.SetConfigType("yaml")
viper.AddConfigPath("./configurations")
err := viper.ReadInConfig()
if err != nil {
log.Fatal("config error : ", err.Error())
}
}
view raw config.go hosted with ❤ by GitHub

...

Database connection

We will define the database connection package in the database.go file, it contains a function to initiate a MySQL database connection with the configuration we took from viper earlier.

Path: food-app/pkg/database

package database
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
"github.com/spf13/viper"
)
func InitDB() (*gorm.DB, error) {
username := viper.GetString("Database.Username")
password := viper.GetString("Database.Password")
host := viper.GetString("Database.Host")
port := viper.GetInt("Database.Port")
dbname := viper.GetString("Database.DBName")
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", username, password, host, port, dbname)
db, err := gorm.Open("mysql",dsn)
if err != nil {
return nil, err
}
if err := db.DB().Ping(); err != nil {
return nil, err
}
return db, nil
}
view raw database.go hosted with ❤ by GitHub

...

Token management

In this application we will use jwt-token to store individual information, verification, and validation

The jwttoken package we define in the token.go file, contains functions to create a jwttoken, extract the contents of the information stored in the jwttoken, and also validate the jwttoken sent from the client.

Path: food-app/pgk/jwttoken

package jwttoken
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/spf13/viper"
)
type TokenDetail struct {
AccessToken string
ExpiredToken int64
}
type AccessDetail struct {
UserID int64
Authorized bool
}
func CreateToken(userid int64) (*TokenDetail, error) {
td := &TokenDetail{}
td.ExpiredToken = time.Now().Add(time.Minute*15).Unix()
var err error
atClaims := jwt.MapClaims{}
atClaims["authorized"] = true
atClaims["user_id"] = userid
atClaims["exp"] = td.ExpiredToken
at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
td.AccessToken, err = at.SignedString([]byte(viper.GetString("Jwt.Secret")))
if err != nil {
return nil, err
}
return td, nil
}
func ExtractToken(r *http.Request) string {
token := r.Header.Get("Authorization")
strArr := strings.Split(token, " ")
if len(strArr) == 2 {
return strArr[1]
}
return ""
}
func VerifyToken(r *http.Request) (*jwt.Token, error) {
tokenString := ExtractToken(r)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Wrong signature method")
}
return []byte(viper.GetString("Jwt.Secret")), nil
})
if err != nil {
return nil, err
}
return token, nil
}
func TokenValid(r *http.Request) error {
token, err := VerifyToken(r)
if err != nil {
return err
}
if _,ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
return err
}
return nil
}
func ExtractTokenMetadata(r *http.Request) (*AccessDetail, error) {
token, err := VerifyToken(r)
if err != nil {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if ok && token.Valid {
authorized, ok := claims["authorized"].(bool)
if !ok {
return nil, err
}
userId := int64(claims["user_id"].(float64))
return &AccessDetail{
Authorized: authorized,
UserID: userId,
}, nil
}
return nil, err
}
view raw token.go hosted with ❤ by GitHub

...

Password management

In this application, the user password will be stored in the database in the form of hash data or the implementation of Provos and Mazières bcrypt adaptive hashing algorithm. Link

File hash.go
Path: food-app/pkg/security

package security
import "golang.org/x/crypto/bcrypt"
func Hash(password string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
}
func VerifyPassword(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
view raw hash.go hosted with ❤ by GitHub

...

Response custom

This custom Response Package will handle returning data according to certain needs and conditions

File response.go
Path: food-app/pkg/response

package response
import (
"net/http"
"github.com/gin-gonic/gin"
)
type ResponseOKWithDataModel struct {
Code int `json:"code"`
Data interface{} `json:"data"`
Message string `json:"message"`
}
type ResponseOKModel struct {
Code int `json:"code"`
Message string `json:"message"`
}
type ResponseErrorModel struct {
Code int `json:"code"`
Message string `json:"message"`
}
type ResponseErrorCustomModel struct {
Code int `json:"code"`
Message interface{} `json:"message"`
}
func ResponseOKWithData(c *gin.Context, data interface{}) {
response := ResponseOKWithDataModel{
Code: 1000,
Data: data,
Message: "OK",
}
c.JSON(http.StatusOK, response)
}
func ResponseCreated(c *gin.Context, data interface{}) {
response := ResponseOKWithDataModel{
Code: 1000,
Data: data,
Message: "Created",
}
c.JSON(http.StatusCreated, response)
}
func ResponseOK(c *gin.Context, message string) {
response := ResponseOKModel{
Code: 1000,
Message: message,
}
c.JSON(http.StatusOK, response)
}
func ResponseError(c *gin.Context, err string, code int) {
response := ResponseErrorModel{
Code: 99,
Message: err,
}
c.JSON(code, response)
}
func ResponseCustomError(c *gin.Context, err interface{}, code int) {
response := ResponseErrorCustomModel{
Code: 99,
Message: err,
}
c.JSON(code, response)
}
view raw response.go hosted with ❤ by GitHub

...

Domain Layer

At this layer start to enter the business logic of this application

Entity

We will function the Entity package as a blueprint for the domain, where we put the schema and struct model for food and users

File: food.go
Path: food-app/internal/domain/entity

package entity
import "strings"
type Food struct {
ID int `json:"id"`
UserID int `json:"user_id"`
Title string `json:"title"`
Description string `json:"description"`
FoodImage string `json:"food_image"`
}
type FoodDetailViewModel struct {
Title string `json"title"`
Description string `json:"description"`
FoodImage string `json:"food_image"`
UserName string `json:"user_name"`
}
type FoodViewModel struct {
ID int `json:"id"`
UserID int `json:"user_id"`
Title string `json"title"`
Description string `json:"description"`
FoodImage string `json:"food_image"`
}
func (f *FoodViewModel) Validate(action string) map[string]string {
var errorMessages = make(map[string]string)
switch strings.ToLower(action) {
case "update":
if f.Title == "" || f.Title == "null" {
errorMessages["title_required"] = "title is required"
}
if f.Description == "" || f.Description == "null" {
errorMessages["desc_required"] = "description is required"
}
default:
if f.Title == "" || f.Title == "null" {
errorMessages["title_required"] = "title is required"
}
if f.Description == "" || f.Description == "null" {
errorMessages["desc_required"] = "description is required"
}
}
return errorMessages
}
view raw food.go hosted with ❤ by GitHub

File: user.go
Path: food-app/internal/domain/entity

package entity
import (
"food-app/pkg/security"
"github.com/badoux/checkmail"
)
type User struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Password string `json:"password"`
}
type ReqisterViewModel struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Password string `json:"password"`
}
type LoginViewModel struct {
Email string `json:"email"`
Password string `json:"password"`
}
type UserViewModel struct {
ID int `json:"id"`
FullName string `json:"full_name"`
Email string `json:"email"`
}
func (u *User) EncryptPassword(password string) (string, error) {
hashPassword, err := security.Hash(password)
if err != nil {
return "", err
}
return string(hashPassword), nil
}
func (u *User) Validate() map[string]string {
var errorMessages = make(map[string]string)
var err error
if u.Email == "" {
errorMessages["email_required"] = "email required"
}
if u.Email != "" {
if err = checkmail.ValidateFormat(u.Email); err != nil {
errorMessages["invalid_email"] = "email email"
}
}
return errorMessages
}
func (u *LoginViewModel) Validate() map[string]string {
var errorMessages = make(map[string]string)
var err error
if u.Password == "" {
errorMessages["password_required"] = "password is required"
}
if u.Email == "" {
errorMessages["email_required"] = "email is required"
}
if u.Email != "" {
if err = checkmail.ValidateFormat(u.Email); err != nil {
errorMessages["invalid_email"] = "please provide a valid email"
}
}
return errorMessages
}
func (u *ReqisterViewModel) Validate() map[string]string {
var errorMessages = make(map[string]string)
var err error
if u.FirstName == "" {
errorMessages["firstname_required"] = "first name is required"
}
if u.LastName == "" {
errorMessages["lastname_required"] = "last name is required"
}
if u.Password == "" {
errorMessages["password_required"] = "password is required"
}
if u.Password != "" && len(u.Password) < 6 {
errorMessages["invalid_password"] = "password should be at least 6 characters"
}
if u.Email == "" {
errorMessages["email_required"] = "email is required"
}
if u.Email != "" {
if err = checkmail.ValidateFormat(u.Email); err != nil {
errorMessages["invalid_email"] = "please provide a valid email"
}
}
return errorMessages
}
view raw user.go hosted with ❤ by GitHub

Pay attention to the user entity, we have implemented a security package for user password encryption

...

Repository

The Package Repository will function as a place to define a collection of methods, which will later interact with databases, services, and other API integrations

File: food_repository.go
Path: food-app/internal/domain/repository

package repository
import (
"food-app/internal/domain/entity"
"github.com/jinzhu/gorm"
)
type FoodRepository struct {
db *gorm.DB
}
type IFoodRepository interface {
SaveFood(*entity.Food) (*entity.Food, error)
GetDetailFood(int) (*entity.Food, error)
GetAllFood() ([]entity.Food, error)
UpdateFood(*entity.Food) (*entity.Food, error)
DeleteFood(int) error
}
func NewFoodRepository(db *gorm.DB) *FoodRepository {
var foodRepo = FoodRepository{}
foodRepo.db = db
return &foodRepo
}
func (r *FoodRepository) SaveFood(food *entity.Food) (*entity.Food, error) {
err := r.db.Create(&food).Error
if err != nil {
return nil, err
}
return food, nil
}
func (r *FoodRepository) GetDetailFood(id int) (*entity.Food, error) {
var food entity.Food
err := r.db.Where("id = ?", id).Take(&food).Error
if err != nil {
return nil, err
}
return &food, nil
}
func (r *FoodRepository) GetAllFood() ([]entity.Food, error) {
var foods []entity.Food
err := r.db.Order("id desc").Find(&foods).Error
if err != nil {
return nil, err
}
return foods, nil
}
func (r *FoodRepository) UpdateFood(food *entity.Food) (*entity.Food, error) {
err := r.db.Save(&food).Error
if err != nil {
return nil, err
}
return food, nil
}
func (r *FoodRepository) DeleteFood(id int) error {
var food entity.Food
err := r.db.Where("id = ?", id).Delete(&food).Error
if err != nil {
return err
}
return nil
}

File: user_repository.go
Path: food-app/internal/domain/repository

package repository
import (
"fmt"
"food-app/internal/domain/entity"
"github.com/jinzhu/gorm"
)
type UserRepository struct {
db *gorm.DB
}
type IUserRepository interface {
SaveUser(*entity.User) (*entity.User, error)
GetDetailUser(int) (*entity.User, error)
GetAllUser() ([]entity.User, error)
UpdateUser(*entity.User) (*entity.User, error)
DeleteUser(int) error
GetUserName(id int) string
GetUserByEmailPassword(loginVM entity.LoginViewModel) (*entity.User, error)
}
func NewUserRepository(db *gorm.DB) *UserRepository {
var userRepo = UserRepository{}
userRepo.db = db
return &userRepo
}
func (r *UserRepository) SaveUser(user *entity.User) (*entity.User, error) {
err := r.db.Create(&user).Error
if err != nil {
return nil, err
}
return user, nil
}
func (r *UserRepository) GetDetailUser(id int) (*entity.User, error) {
var user entity.User
err := r.db.Where("id = ?", id).Take(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepository) GetUserName(id int) string {
userDetail, _ := r.GetDetailUser(id)
var fullname = fmt.Sprintf("%s %s", userDetail.FirstName, userDetail.LastName)
return fullname
}
func (r *UserRepository) GetAllUser() ([]entity.User, error) {
var users []entity.User
err := r.db.Order("id desc").Find(&users).Error
if err != nil {
return nil, err
}
return users, nil
}
func (r *UserRepository) UpdateUser(user *entity.User) (*entity.User, error) {
err := r.db.Save(&user).Error
if err != nil {
return nil, err
}
return user, nil
}
func (r *UserRepository) DeleteUser(id int) error {
var user entity.User
err := r.db.Where("id = ?", id).Delete(&user).Error
if err != nil {
return err
}
return nil
}
func (r *UserRepository) GetUserByEmailPassword(loginVM entity.LoginViewModel) (*entity.User, error) {
var user entity.User
err := r.db.Where("email = ?", loginVM.Email).Take(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}

...

Service

The Package Service will serve as the place where we implement the methods defined in the repository
File: food_service.go
Path: food-app/internal/domain/service

package service
import (
"food-app/internal/domain/entity"
"food-app/internal/domain/repository"
)
type FoodService struct {
foodRepo repository.IFoodRepository
userRepo repository.IUserRepository
}
type IFoodService interface {
SaveFood(*entity.FoodViewModel) (*entity.FoodViewModel, error)
GetDetailFood(int) (*entity.FoodDetailViewModel, error)
GetAllFood() ([]entity.FoodDetailViewModel, error)
UpdateFood(*entity.FoodViewModel) (*entity.FoodViewModel, error)
DeleteFood(int) error
}
func NewFoodService(foodRepo repository.IFoodRepository, userRepo repository.IUserRepository) *FoodService {
var foodService = FoodService{}
foodService.foodRepo = foodRepo
foodService.userRepo = userRepo
return &foodService
}
func (s *FoodService) SaveFood(foodVM *entity.FoodViewModel) (*entity.FoodViewModel, error) {
var food = entity.Food{
UserID: foodVM.UserID,
Title: foodVM.Title,
Description: foodVM.Description,
FoodImage: foodVM.FoodImage,
}
result, err := s.foodRepo.SaveFood(&food)
if err != nil {
return nil, err
}
if result != nil {
foodVM = &entity.FoodViewModel{
ID: result.ID,
UserID: result.UserID,
Title: result.Title,
Description: result.Description,
FoodImage: result.FoodImage,
}
}
return foodVM, nil
}
func (s *FoodService) GetDetailFood(id int) (*entity.FoodDetailViewModel, error) {
result, err := s.foodRepo.GetDetailFood(id)
if err != nil {
return nil, err
}
var foodVM entity.FoodDetailViewModel
if result != nil {
foodVM = entity.FoodDetailViewModel{
UserName: s.userRepo.GetUserName(result.UserID),
Title: result.Title,
FoodImage: result.FoodImage,
Description: result.Description,
}
}
return &foodVM, nil
}
func (s *FoodService) GetAllFood() ([]entity.FoodDetailViewModel, error) {
result, err := s.foodRepo.GetAllFood()
if err != nil {
return nil, err
}
var foodListVM []entity.FoodDetailViewModel
if result != nil {
for _, item := range result {
foodVM := entity.FoodDetailViewModel{
UserName: s.userRepo.GetUserName(item.UserID),
Title: item.Title,
FoodImage: item.FoodImage,
Description: item.Description,
}
foodListVM = append(foodListVM, foodVM)
}
}
return foodListVM, nil
}
func (s *FoodService) UpdateFood(foodVM *entity.FoodViewModel) (*entity.FoodViewModel, error) {
var food = entity.Food{
ID: foodVM.ID,
UserID: foodVM.UserID,
Title: foodVM.Title,
Description: foodVM.Description,
FoodImage: foodVM.FoodImage,
}
_, err := s.foodRepo.UpdateFood(&food)
if err != nil {
return nil, err
}
return foodVM, nil
}
func (s *FoodService) DeleteFood(id int) error {
err := s.foodRepo.DeleteFood(id)
if err != nil {
return err
}
return nil
}
view raw food_service.go hosted with ❤ by GitHub

File: user_service.go
Path: food-app/internal/domain/service

package service
import (
"fmt"
"food-app/internal/domain/entity"
"food-app/internal/domain/repository"
"food-app/pkg/security"
"golang.org/x/crypto/bcrypt"
)
type UserService struct {
userRepo repository.IUserRepository
}
type IUserService interface {
SaveUser(*entity.ReqisterViewModel) (*entity.UserViewModel, error)
GetListUser() (*[]entity.UserViewModel, error)
GetDetailUser(id int) (*entity.UserViewModel, error)
UpdateUser(userVM *entity.User) (*entity.UserViewModel, error)
DeleteUser(id int) error
GetUserByEmailPassword(loginVM entity.LoginViewModel) (*entity.User, error)
}
func NewUserService(userRepo repository.IUserRepository) *UserService {
var userService = UserService{}
userService.userRepo = userRepo
return &userService
}
func (s *UserService) GetListUser() (*[]entity.UserViewModel, error) {
result, err := s.userRepo.GetAllUser()
if err != nil {
return nil, err
}
var users []entity.UserViewModel
for _, item := range result {
var user entity.UserViewModel
user.Email = item.Email
user.FullName = fmt.Sprintf("%s %s", item.FirstName, item.LastName)
user.Email = item.Email
users = append(users, user)
}
return &users, nil
}
func (s *UserService) GetDetailUser(id int) (*entity.UserViewModel, error) {
var viewModel entity.UserViewModel
result, err := s.userRepo.GetDetailUser(id)
if err != nil {
return nil, err
}
if result != nil {
viewModel = entity.UserViewModel{
ID: result.ID,
FullName: fmt.Sprintf("%s %s", result.FirstName, result.LastName),
Email: result.Email,
}
}
return &viewModel, nil
}
func (s *UserService) SaveUser(userVM *entity.ReqisterViewModel) (*entity.UserViewModel, error) {
var user = entity.User{
FirstName: userVM.FirstName,
LastName: userVM.LastName,
Email: userVM.Email,
}
password, err := user.EncryptPassword(userVM.Password)
if err != nil {
return nil, err
}
user.Password = password
result, err := s.userRepo.SaveUser(&user)
if err != nil {
return nil, err
}
var afterRegVM entity.UserViewModel
if result != nil {
afterRegVM = entity.UserViewModel{
ID: result.ID,
FullName: fmt.Sprintf("%s %s", result.FirstName, result.LastName),
Email: result.Email,
}
}
return &afterRegVM, nil
}
func (s *UserService) UpdateUser(userVM *entity.User) (*entity.UserViewModel, error) {
password, err := userVM.EncryptPassword(userVM.Password)
if err != nil {
return nil, err
}
userVM.Password = password
result, err := s.userRepo.UpdateUser(userVM)
if err != nil {
return nil, err
}
var userAfterUpdate entity.UserViewModel
userAfterUpdate = entity.UserViewModel{
ID: result.ID,
FullName: fmt.Sprintf("%s %s", result.FirstName, result.LastName),
Email: result.Email,
}
return &userAfterUpdate, err
}
func (s *UserService) DeleteUser(id int) error {
err := s.userRepo.DeleteUser(id)
if err != nil {
return err
}
return nil
}
func (s *UserService) GetUserByEmailPassword(loginVM entity.LoginViewModel) (*entity.User, error) {
result, err := s.userRepo.GetUserByEmailPassword(loginVM)
if err != nil {
return nil, err
}
// Verify Password
err = security.VerifyPassword(result.Password, loginVM.Password)
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
return nil, fmt.Errorf("Incorrect Password. Error %s", err.Error())
}
return result, nil
}
view raw user_service.go hosted with ❤ by GitHub

...

Handler

The Package Handler here is where we handle HTTP requests and responses. The entry point for user-related services, and food-related services

File: food_handler.go
Path: food-app/internal/domain/handler

package handler
import (
"fmt"
"food-app/internal/domain/entity"
"food-app/internal/domain/service"
"food-app/pkg/jwttoken"
"food-app/pkg/response"
"net/http"
"path/filepath"
"strconv"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
type FoodHandler struct {
foodService service.IFoodService
}
func NewFoodHandler(foodService service.IFoodService) *FoodHandler {
var foodHandler = FoodHandler{}
foodHandler.foodService = foodService
return &foodHandler
}
func (h *FoodHandler) SaveFood(c *gin.Context) {
title := c.DefaultPostForm("title","title")
description := c.DefaultPostForm("description","description")
tokenMetadata, err := jwttoken.ExtractTokenMetadata(c.Request)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
userId := tokenMetadata.UserID
file, err := c.FormFile("file")
if err != nil {
response.ResponseError(c, err.Error(), http.StatusBadRequest)
return
}
path := viper.GetString("Files.Path")
filename := filepath.Base(file.Filename)
if err := c.SaveUploadedFile(file, fmt.Sprintf("%s/%s", path, filename)); err != nil {
response.ResponseError(c, err.Error(), http.StatusBadRequest)
return
}
var food = entity.FoodViewModel{
UserID: int(userId),
Title: title,
Description: description,
FoodImage: filename,
}
saveFoodError := food.Validate("")
if len(saveFoodError) > 0 {
response.ResponseCustomError(c, saveFoodError, http.StatusBadRequest)
return
}
result, err := h.foodService.SaveFood(&food)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
}
response.ResponseCreated(c, result)
}
func (h *FoodHandler) GetAllFood(c *gin.Context) {
result, err := h.foodService.GetAllFood()
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
if result == nil {
result = []entity.FoodDetailViewModel{}
}
response.ResponseOKWithData(c, result)
}
func (h *FoodHandler) GetDetailFood(c *gin.Context) {
foodId, err := strconv.Atoi(c.Param("food_id"))
if err != nil {
response.ResponseError(c, err.Error(), http.StatusBadRequest)
return
}
result, err := h.foodService.GetDetailFood(foodId)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
response.ResponseOKWithData(c, result)
}
func (h *FoodHandler) UpdateFood(c *gin.Context) {
foodId, err := strconv.Atoi(c.Param("food_id"))
if err != nil {
response.ResponseError(c, err.Error(), http.StatusBadRequest)
return
}
title := c.DefaultPostForm("title", "title")
description := c.DefaultPostForm("description", "description")
userId := 0
// Source
file, err := c.FormFile("file")
if err != nil {
response.ResponseError(c, err.Error(), http.StatusBadRequest)
return
}
filename := filepath.Base(file.Filename)
if err := c.SaveUploadedFile(file, filename); err != nil {
response.ResponseError(c, err.Error(), http.StatusBadRequest)
return
}
var food = entity.FoodViewModel{
ID: foodId,
UserID: userId,
Title: title,
Description: description,
FoodImage: filename,
}
saveFoodError := food.Validate("")
if len(saveFoodError) > 0 {
response.ResponseCustomError(c, saveFoodError, http.StatusBadRequest)
return
}
result, err := h.foodService.UpdateFood(&food)
if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, result)
}
func (h *FoodHandler) DeleteFood(c *gin.Context) {
foodId, err := strconv.Atoi(c.Param("food_id"))
if err != nil {
response.ResponseError(c, err.Error(), http.StatusBadRequest)
return
}
err = h.foodService.DeleteFood(foodId)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
response.ResponseOK(c, "Successfully Food Deleted")
}
view raw food_handler.go hosted with ❤ by GitHub

File: user_handler.go
Path: food-app/internal/domain/handler

package handler
import (
"errors"
"food-app/internal/domain/entity"
"food-app/internal/domain/service"
"food-app/pkg/jwttoken"
"food-app/pkg/response"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type UserHandler struct {
userService service.IUserService
}
func NewUserHandler(userService service.IUserService) *UserHandler {
var userHandler = UserHandler{}
userHandler.userService = userService
return &userHandler
}
func (h *UserHandler) RegisterUser(c *gin.Context) {
var registerUser entity.ReqisterViewModel
err := c.ShouldBindJSON(&registerUser)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
registerUserError := registerUser.Validate()
if len(registerUserError) > 0 {
response.ResponseCustomError(c, registerUserError, http.StatusBadRequest)
return
}
result, err := h.userService.SaveUser(&registerUser)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
response.ResponseCreated(c, result)
}
func (h *UserHandler) GetAllUser(c *gin.Context) {
result, err := h.userService.GetListUser()
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
if result == nil {
result = &[]entity.UserViewModel{}
}
response.ResponseOKWithData(c, result)
}
func (h *UserHandler) GetDetailUser(c *gin.Context) {
userId, err := strconv.Atoi(c.Param("user_id"))
if err != nil {
response.ResponseError(c, err.Error(), http.StatusBadRequest)
return
}
result, err := h.userService.GetDetailUser(userId)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
if result == nil {
result = &entity.UserViewModel{}
}
response.ResponseOKWithData(c, result)
}
func (h *UserHandler) UpdateUser(c *gin.Context) {
userId, err := strconv.Atoi(c.Param("user_id"))
if err != nil {
response.ResponseError(c, errors.New("Invalid User ID").Error(), http.StatusBadRequest)
return
}
var updateUser entity.User
err = c.ShouldBindJSON(&updateUser)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
updateUser.ID = userId
updateUserError := updateUser.Validate()
if len(updateUserError) > 0 {
response.ResponseCustomError(c, updateUserError, http.StatusBadRequest)
return
}
result, err := h.userService.UpdateUser(&updateUser)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
if result == nil {
result = &entity.UserViewModel{}
}
response.ResponseOKWithData(c, result)
}
func (h *UserHandler) DeleteUser(c *gin.Context) {
userId, err := strconv.Atoi(c.Param("user_id"))
if err != nil {
response.ResponseError(c, errors.New("Invalid User ID").Error(), http.StatusBadRequest)
return
}
err = h.userService.DeleteUser(userId)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
response.ResponseOK(c, "Successfully User Deleted")
}
func (h *UserHandler) Login(c *gin.Context) {
var loginVM entity.LoginViewModel
err := c.ShouldBindJSON(&loginVM)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusUnprocessableEntity)
return
}
validateUser, err := h.userService.GetUserByEmailPassword(loginVM)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
if validateUser == nil {
validateUser = &entity.User{}
}
// Generete JWT
token, err := jwttoken.CreateToken(int64(validateUser.ID))
if err != nil {
response.ResponseError(c, err.Error(), http.StatusInternalServerError)
return
}
userData := map[string]interface{}{
"access_token": token.AccessToken,
"expired": token.ExpiredToken,
"user_id": validateUser.ID,
}
response.ResponseOKWithData(c, userData)
}
view raw user_handler.go hosted with ❤ by GitHub

...

API Layer

Middleware

Package Middleware here functions as an in-between process for us to pass requests from authenticated users and restrict/hold access from unauthenticated users. Here we also define CORS middleware Link so that users can access data from different domains

File: middleware.go
Path: food-app/api/middleware

package middleware
import (
"food-app/pkg/jwttoken"
"food-app/pkg/response"
"net/http"
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := jwttoken.TokenValid(c.Request)
if err != nil {
response.ResponseError(c, err.Error(), http.StatusUnauthorized)
c.Abort()
return
}
c.Next()
}
}
func CORSMiddleware() gin.HandlerFunc {
return func (c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin","*")
c.Writer.Header().Set("Access-Control-Allow-Credentials","true")
c.Writer.Header().Set("Access-Control-Allow-Headers","Content-Type,Content-Length")
c.Writer.Header().Set("Access-Control-Allow-Method","POST, GET, DELETE, PUT")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
view raw middleware.go hosted with ❤ by GitHub

...

Router

In this Router package we will define our API routes, and mapping the response, also include the middleware earlier

File : router.go
Path : food-app/api

package api
import (
"food-app/api/middleware"
"food-app/internal/domain/handler"
"food-app/internal/domain/repository"
"food-app/internal/domain/service"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
)
func SetupRouter(db *gorm.DB) *gin.Engine {
router := gin.Default()
//Register User Repo
userRepo := repository.NewUserRepository(db)
userService := service.NewUserService(userRepo)
userHandler := handler.NewUserHandler(userService)
//
foodRepo := repository.NewFoodRepository(db)
foodService := service.NewFoodService(foodRepo, userRepo)
foodHandler := handler.NewFoodHandler(foodService)
food := router.Group("v1/food")
{
food.GET("/", middleware.AuthMiddleware(), middleware.CORSMiddleware(), foodHandler.GetAllFood)
food.GET("/:food_id", middleware.AuthMiddleware(), middleware.CORSMiddleware(), foodHandler.GetDetailFood)
food.POST("/", middleware.AuthMiddleware(), middleware.CORSMiddleware(), foodHandler.SaveFood)
food.PUT("/:food_id", middleware.CORSMiddleware(), foodHandler.UpdateFood)
food.DELETE("/:food_id", middleware.CORSMiddleware(), foodHandler.DeleteFood)
}
user := router.Group("v1/user")
{
user.GET("/", middleware.CORSMiddleware(), userHandler.GetAllUser)
user.GET("/:user_id", middleware.AuthMiddleware(), middleware.CORSMiddleware(), userHandler.GetDetailUser)
user.POST("/", middleware.CORSMiddleware(), userHandler.RegisterUser)
user.PUT("/:user_id", middleware.CORSMiddleware(), userHandler.UpdateUser)
user.DELETE("/:user_id", middleware.CORSMiddleware(), userHandler.DeleteUser)
user.POST("/login", middleware.CORSMiddleware(), userHandler.Login)
}
return router
}
view raw router.go hosted with ❤ by GitHub

...

Migration

In this application, we have given ddl for the MySQL client to initiate the tables that we are using, please access the food-app.sql file, at the path: food-app/migration

Run the command in it on your local MySQL database client

...

Main.go

In this main package, we will initiate the environment config and open a connection to the database, after success, the application will run locally on the port we specified in the environment.

package main
import (
"fmt"
"food-app/api"
"food-app/pkg/config"
"food-app/pkg/database"
"log"
"github.com/spf13/viper"
)
func init() {
config.GetConfig()
}
func main() {
db, err := database.InitDB()
if err != nil {
log.Fatal(err)
return
}
defer db.Close()
port := fmt.Sprintf(":%d",viper.GetInt("App.Port"))
app := api.SetupRouter(db)
app.Run(port)
}
view raw main.go hosted with ❤ by GitHub

...

Run application

Make sure your database has been created with dbname according to the environment, the tables have been formed after running the sql command in the migration folder, to run this application do the following command

go mod init
go get
go run main.go

...

That is all and thank you

Good luck

Credits and Insights

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay