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
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" |
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()) | |
} | |
} |
...
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 | |
} |
...
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 | |
} |
...
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)) | |
} |
...
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) | |
} |
...
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 | |
} |
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 | |
} |
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 | |
} |
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 | |
} |
...
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") | |
} |
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(®isterUser) | |
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(®isterUser) | |
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) | |
} |
...
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() | |
} | |
} |
...
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 | |
} |
...
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) | |
} |
...
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
Top comments (0)