We're planting a tree for every job application! Click here to learn more

Domain Driven Design with Go, Fiber, Mysql and Docker

Titus Dishon

6 Oct 2021

•

8 min read

Domain Driven Design with Go, Fiber, Mysql and Docker
  • Docker

Introduction

golang_domain_driven_architecture.png In this article, I will be using the above model to implement Domain-driven design with the go programming language. What is domain-driven design?

Domain-driven design involves dividing the whole model into smaller easy to manage and change sub-models. In our case the process of adding a new user to the system has four sub-layers:

  • Application layer: Contains our routes.
  • Controllers layer: Transfers data between the controllers and services layer.
  • Services Layer: This contains the code that implements our business logic.
  • Domain layer: We will be dividing this layer into two sub-layers.
    • Data access objects(DAO): This layer contains the basic CRUD operations for one entity class.
    • Data Transfer Objects: Objects that mirror database schemes. Here we define our entities and their data structure.

Why should I use the structure above? Closely observing the structure, you will realize that we can change the web framework in use without touching the domain and services layer, you can as well change the datastore in use by changing the Domain layer-> DAO file. When one is required to change only some business logic, they will have to change the files in the services layer only. This makes the code to be maintainable and easy to scale up especially when the maintainer is different from the original author.

Prerequisites for the project:

With all that in mind let's start to create the authentication system.#### Project structure

  • Create a folder with your desired name
  • In the root of the folder run git mod init example.com This will create a go.mod file which contains the list of packages you will install on your project. -Create other two files:
     - `Dockerfile `: For docker configurations
     - `docker-compose.yaml`:  For docker-compose configurations, docker-compose will be used to run the project as it abstracts the different docker commands with simple ones.
    
    Paste the code below in the Dockerfile
FROM golang1.16
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
CMD ["air"]

and for the docker-compose.yaml file, paste the code below.

version: "3.9"

services:
  go-domain-driven-dev-auth-service:
    build: .
    ports:
      - "9000:9000"
    volumes:
      - .:/app
    depends_on:
      - db
  # database connection and creation
  db:
    image: mysql:5.7.22
    build: ./mysql-db
    restart: always
    environment:
      MYSQL_DATABASE: go-domain-driven-dev-auth-service
      MYSQL_USER: root
      MYSQL_PASSWORD: root
      MYSQL_ROOT_PASSWORD: root
    volumes:
      - .dbdata:/var/lib/mysql
    # map mysql port to a different port
    ports:
      - "33066:3306"

Create a folder databasein the root of the project, and add three files : connection.go: Will connect us to the database.

package database

import (
	"database/sql"
	"os"

	_ "github.com/go-sql-driver/mysql"
)

var MYDB *sql.DB

func ConnectToMysql() {
	var err error
	dsn := os.Getenv("MYSQLDB")
	MYDB, err = sql.Open("mysql", dsn)
	if err != nil {
		panic("Could not connect to mysql database!!")
	}
}

create a .env file in your project root directory and add environment variables

MYSQLDB=username:password@tcp(docker_container_name:3306)/database_name?charset=utf8mb4&parseTime=True&loc=Local
SECRET_KEY=somesecretkeyhere

Create a folder middlewares the root of the project, and add file : auth_middleware.go: Will contain authentication middlewares like a function to generate jwt token and check authentication status. auth_middleware.go

package middlewares

import (
	"github.com/dgrijalva/jwt-go"
	"github.com/gofiber/fiber/v2"
	"os"
	"time"
)

type Claims struct {
	Email string `json:"email,omitempty"`
	Scope string
	jwt.StandardClaims
}

func GenerateJWT(id int, email string) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, &Claims{
		Email: email,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
			Subject:   string(id),
		},
	})
	return token.SignedString([]byte(os.Getenv("SECRET_KEY")))
}
func IsAuthenticated(c *fiber.Ctx) error {
	cookie := c.Cookies("jwt")
	token, err := jwt.ParseWithClaims(cookie, &Claims{}, func(t *jwt.Token) (interface{}, error) {
		return []byte(os.Getenv("SECRETLY")), nil
	})
	if err != nil || !token.Valid {
		c.Status(fiber.StatusUnauthorized)
		return c.JSON(
			fiber.Map{
				"message": "unauthenticated",
			})
	}
	return c.Next()
}

Create a folder domain in the root of the project, and add three files : user_dao.go: Will contain the methods to act on entities. user_dto.go: will contain our entity definition. marshal.go: This will be used to return a list of users. It can also be used to differentiate a private user data object from a public user data object.

Paste the code below to the user_dto.go file and the routes.go files: user_dto.go

package domain

import (
	"golang.org/x/crypto/bcrypt"
	"strings"
	"time"
)

type User struct {
	ID           int       `json:"id"`
	FullName     string    `json:"full_name"`
	Email        string    `json:"email"`
	PhoneNumber  string    `json:"phone_number"`
	Password     []byte    `json:"-"`
	DateCreated  time.Time `json:"date_created"`
	DateModified time.Time `json:"date_modified"`
}
type Users []User

type LoginRequest struct {
	Email    string `json:"email"`
	Password string `json:"-"`
}
// SetPassword: sets the hased password to the user struct defined above
func (user *User) SetPassword(password string) {
	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(strings.TrimSpace(password)), 12)
	user.Password = hashedPassword
}

// ComparePassword: Used to compare user stored password and  login  password 
func (user *User) ComparePassword(password string) error {
	return bcrypt.CompareHashAndPassword(user.Password, []byte(strings.TrimSpace(password)))
}

In the user_dao.go paste the code below:

package domain

import (
	"database/sql"
	"go-domain-driven-dev-auth-service/database"
	"log"
)

var (
	createUserQuery                = `INSERT INTO users(full_name, email, phone_number, password, date_created) VALUES(?,?,?,?,?)`
	getUserQuery                   = `SELECT user.full_name, user.email, user.phone_number, user.date_created, user.date_modified FROM users as user WHERE user.id=?`
	getAllUsersQuery               = `SELECT user.full_name, user.email, user.phone_number, user.date_created, user.date_modified FROM users as user`
	updateUserQuery                = `UPDATE users SET full_name=?, email=?, phone_number=?, date_modified=? WHERE user.id=?`
	getUserByEmailAndPasswordQuery = `SELECT user.full_name, user.email, user.phone_number, user.date_created, user.date_modified FROM users as user WHERE user.email=?`
)

func (user *User) Save() error {

	stmt, err := database.MYDB.Prepare(createUserQuery)
	if err != nil {
		return err
	}
	defer func(stmt *sql.Stmt) {
		err := stmt.Close()
		if err != nil {
			log.Fatalf(err.Error())
		}
	}(stmt)

	res, saveErr := stmt.Exec(
		user.FullName,
		user.Email,
		user.PhoneNumber,
		user.Password,
		user.DateCreated)
	if saveErr != nil {
		return err
	}
	userId, err := res.LastInsertId()
	if err != nil {
		log.Printf("Error when getting the last inserted id for user %s", err)
		return err
	}
	user.ID = int(userId)
	return nil
}

func (user *User) Get() error {
	stmt, err := database.MYDB.Prepare(getUserQuery)
	if err != nil {
		return err
	}
	defer func(stmt *sql.Stmt) {
		err := stmt.Close()
		if err != nil {
			log.Fatalf(err.Error())
		}
	}(stmt)
	result := stmt.QueryRow(user.ID)
	if getErr := result.Scan(
		&user.FullName,
		&user.Email,
		&user.PhoneNumber,
		&user.DateCreated,
		&user.DateModified,
		&user.ID); getErr != nil {
		return getErr
	}
	return nil
}

func (user *User) GetUsers() ([]User, error) {
	stmt, err := database.MYDB.Prepare(getAllUsersQuery)
	if err != nil {
		return nil, err
	}
	defer func(stmt *sql.Stmt) error {
		err := stmt.Close()
		if err != nil {
			return err
		}
		return nil
	}(stmt)
	rows, err := stmt.Query()
	if err != nil {
		return nil, err

	}
	defer func(rows *sql.Stmt) {
		err := rows.Close()
		if err != nil {
			log.Fatalf(err.Error())
		}
	}(stmt)
	results := make([]User, 0)
	for rows.Next() {
		var user User
		if err := rows.Scan(
			user.FullName,
			user.Email,
			user.PhoneNumber,
			user.DateCreated,
			user.DateModified,
			user.ID); err != nil {
			return nil, err
		}
		results = append(results, user)
	}

	if len(results) == 0 {
		return nil, err
	}
	return results, nil
}

func (user *User) Update() error {
	stmt, err := database.MYDB.Prepare(updateUserQuery)
	if err != nil {
		return err
	}

	defer func(stmt *sql.Stmt) {
		err := stmt.Close()
		if err != nil {
			log.Fatalf(err.Error())
		}
	}(stmt)

	res, saveErr := stmt.Exec(
		user.FullName,
		user.Email,
		user.PhoneNumber,
		user.Password,
		user.DateModified)
	if saveErr != nil {
		return err
	}
	userId, err := res.LastInsertId()
	if err != nil {
		log.Printf("Error when getting the last inserted id for user %s", err)
		return err
	}
	user.ID = int(userId)
	return nil
}

func (user *User) FindByEmailAndPassword() error {
	stmt, err := database.MYDB.Prepare(getUserByEmailAndPasswordQuery)
	if err != nil {
		return err
	}
	defer func(stmt *sql.Stmt) {
		err := stmt.Close()
		if err != nil {
			log.Fatalf(err.Error())
		}
	}(stmt)
	result := stmt.QueryRow(user.Email)
	if getErr := result.Scan(
		&user.FullName,
		&user.Email,
		&user.PhoneNumber,
		&user.Password,
		&user.ID); getErr != nil {
		log.Fatalf("Error trying to get user by email and password")
		return err
	}
	return nil
}

marshal.go

package domain

import (
	"time"
)

type PrivateUser struct {
	ID           int       `json:"id"`
	FullName     string    `json:"full_name"`
	Email        string    `json:"email"`
	PhoneNumber  string    `json:"phone_number"`
	Password     []byte    `json:"-"`
	DateCreated  time.Time `json:"date_created"`
	DateModified time.Time `json:"date_modified"`
}

type PublicUser struct {
	FullName string `json:"full_name"`
}

func (users Users) Marshall() interface{} {
	result := make([]interface{}, len(users))
	for index, user := range users {
		result[index] = user
	}
	return result
}

Create a folder service in the root of the project, and add files: user_services.go: Will contain the business logic for the user. user_services.go

package services

import "go-domain-driven-dev-auth-service/domain"

type authenticationInterface interface {
	CreateUser(user domain.User) (*domain.User, error)
	GetUser(userId int) (*domain.User, error)
	GetUsers() (domain.Users, error)
	UpdateUser(user domain.User) (*domain.User, error)
	Login(loginRequest domain.LoginRequest) (*domain.User, error)
}
type authService struct{}

var (
	AuthService authenticationInterface = &authService{}
)

func (a authService) CreateUser(user domain.User) (*domain.User, error) {
	user.SetPassword(string(user.Password))
	if err := user.Save(); err != nil {
		return nil, err
	}
	return &user, nil
}

func (a authService) GetUser(userId int) (*domain.User, error) {
	result := &domain.User{ID: userId}
	if err := result.Get(); err != nil {
		return nil, err
	}
	return result, nil
}

func (a authService) GetUsers() (domain.Users, error) {
	results := &domain.User{}
	return results.GetUsers()
}

func (a authService) UpdateUser(user domain.User) (*domain.User, error) {
	if err := user.Update(); err != nil {
		return nil, err
	}
	return &user, nil
}

func (a authService) Login(loginRequest domain.LoginRequest) (*domain.User, error) {
	dao := &domain.User{
		Email:    loginRequest.Email,
		Password: []byte(loginRequest.Password),
	}
	if err := dao.FindByEmailAndPassword(); err != nil {
		return nil, err
	}
	return dao, nil
}

Create a folder service in the root of the project, and add files: user_services.go: Will contain the business logic for the user. user_services.go

package controllers

import (
	"github.com/gofiber/fiber/v2"
	"go-domain-driven-dev-auth-service/domain"
	"go-domain-driven-dev-auth-service/middlewares"
	"go-domain-driven-dev-auth-service/services"
	"net/http"
	"strconv"
	"time"
)

type authControllerInterface interface {
	CreateUser(c *fiber.Ctx) error
	GetUser(c *fiber.Ctx) error
	GetUsers(c *fiber.Ctx) error
	UpdateUser(c *fiber.Ctx) error
	Login(c *fiber.Ctx) error
}

type authControllers struct{}

var (
	AuthControllers authControllerInterface = &authControllers{}
)

func (a authControllers) CreateUser(c *fiber.Ctx) error {
	var user domain.User
	if err:=c.BodyParser(&user); err!=nil {
		return c.JSON(fiber.Map{
				"message":"Invalid JSON body",
			})
	}
	result, saveErr:= services.AuthService.CreateUser(user)
	if saveErr!=nil {
		return c.JSON(saveErr)
	}
	return  c.JSON(fiber.Map{
		"StatusCode": http.StatusOK,
		"message": "succeeded",
		"users": result,
	})
}

func (a authControllers) GetUser(c *fiber.Ctx) error {
	userId, err := strconv.Atoi(c.Params("user_id"))

	if err!=nil {
		return c.JSON(fiber.Map{
			"message":"Invalid user id",
		})
	}
	user, getErr := services.AuthService.GetUser(userId)
	if getErr!=nil {
		return c.JSON(getErr)
	}
	return  c.JSON(fiber.Map{
		"StatusCode": http.StatusOK,
		"message": "succeeded",
		"users": user,
	})
}

func (a authControllers) GetUsers(c *fiber.Ctx) error {
	results, getErr := services.AuthService.GetUsers()
	if getErr!=nil {
		return c.JSON(getErr)
	}
	return  c.JSON(fiber.Map{
		"StatusCode": http.StatusOK,
		"message": "succeeded",
		"users": results,
	})
}

func (a authControllers) UpdateUser(c *fiber.Ctx) error {
	var user domain.User
	if err:=c.BodyParser(&user); err!=nil {
		return c.JSON(fiber.Map{
			"message":"Invalid JSON body",
		})
	}
	result, saveErr:= services.AuthService.UpdateUser(user)
	if saveErr!=nil {
		return c.JSON(saveErr)
	}
	return  c.JSON(fiber.Map{
		"StatusCode": http.StatusOK,
		"message": "succeeded",
		"users": result,
	})
}

func (a authControllers) Login(c *fiber.Ctx) error {
	var loginRequest domain.LoginRequest
	if err:=c.BodyParser(&loginRequest); err!=nil {
		return c.JSON(fiber.Map{
			"message":"Invalid JSON body",
		})
	}
	user, getErr := services.AuthService.Login(loginRequest)
	if getErr!=nil {
		return c.JSON(getErr)
	}
	token, err := middlewares.GenerateJWT(user.ID, user.Email)
	if err!=nil {
		return c.JSON(fiber.Map{
			"message":"We could not log you in at this time, please try again later",
		})
	}
	cookie := fiber.Cookie{
		Name:     "jwt",
		Value:    token,
		Expires:  time.Now().Add(time.Hour * 24),
		HTTPOnly: true,
	}
	c.Cookie(&cookie)
	return  c.JSON(fiber.Map{
		"StatusCode": http.StatusOK,
		"message": "succeeded",
		"users":user,
	})
}

Create a folder application in the root of the project, and add two files : routes.go: Will contain the routes configurations. app.go: will contain our web framework configuration, in my case fiber configuration Paste the code below to the app.go file and the routes.go files:

app.go

package application

import (
	"fmt"
	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
)

var(
	router= fiber.New()
)
func StartApplication()  {
	router.Use(cors.New(cors.Config{
		AllowCredentials: true,
	}))
	MapUrls()
	router.Listen(":9022")
}

and in the application.go file, paste the code below:

func MapUrls()  {
	api := router.Group("api")
	admin := api.Group("admin")
	admin.Post("register", controllers.AuthControllers.CreateUser)
	admin.Post("login", controllers.AuthControllers.Login)
	adminAuthenticated := admin.Use(middlewares.IsAuthenticated)
	adminAuthenticated.Get("users/:userCode", controllers.AuthControllers.GetUser)
	adminAuthenticated.Get("users/search-by-status", controllers.AuthControllers.GetUsers)

}

Finally, create the file server.go in the root of your project to contain the main function: server. go

package main

import (
	"go-domain-driven-dev-auth-service/database"
	"go-domain-driven-dev-auth-service/routes"
	"log"

	"github.com/joho/godotenv"
)

func main() {
       // load environment variables
	err := godotenv.Load(".env")
	if err != nil {
		log.Fatalf("error loading .env file")
	}
       // connect to mysql
	database.ConnectToMysql()
       // Start the application
	routes.StartApplication()
}

Install a visual database design tool like MySQL workbench on your machine and connect to your database, then create the table for users:

To fire up your applications, on your terminal change directory into the project root and run the following command. docker-compose up

  • Your project should now be running on http://localhost:9000
  • You can now go ahead and use postman to test your APIs.
Did you like this article?

Titus Dishon

Hello, Am Titus Dishon. A software engineer based in Nairobi Kenya. I have advanced skills in Full-stack development using Nodejs , golang, reactjs , Javascript and Typescript. I also have experience using the AWS cloud and am a holder of Cloud practitioner certification. On the database side, I have experience working with PostgreSQL , MySQL, DynamoDb and MongoDB. Over the past two years I have heavily developed micro-frontend architecture systems using nx and react. I have also been developing Microservice architecture using AWS Lambdas, knesis , SQS, SNS and DynamoDB. My fun developemtn with AWS Cloud is the concept of infrastructure as code using the SDK. For more about these experiences, feel free to reach out.

See other articles by Titus

Related jobs

See all

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Related articles

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

•

12 Sep 2021

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

•

12 Sep 2021

WorksHub

CareersCompaniesSitemapFunctional WorksBlockchain WorksJavaScript WorksAI WorksGolang WorksJava WorksPython WorksRemote Works
hello@works-hub.com

Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ

108 E 16th Street, New York, NY 10003

Subscribe to our newsletter

Join over 111,000 others and get access to exclusive content, job opportunities and more!

© 2024 WorksHub

Privacy PolicyDeveloped by WorksHub