We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to publish this job!

Login or register
to save this job!

Login or register
to save interesting jobs!

Login or register
to get access to all your job applications!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel 🚀.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Login or register
to save articles!

Login to see the application

Engineers who find a new job through WorksHub average a 15% increase in salary 🚀

You will be redirected back to this page right after signin

Blog hero image

Domain Driven Design with Go, Fiber, Mysql and Docker

Titus Mutiso Dishon 6 October, 2021 | 8 min read

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.comThis 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.
Author's avatar
Titus Mutiso Dishon
Working with javascript, golang and typescript.

Related Issues

open-editions / corpus-joyce-ulysses-tei
open-editions / corpus-joyce-ulysses-tei
  • Open
  • 0
  • 0
  • Intermediate
  • HTML
open-editions / corpus-joyce-ulysses-tei
open-editions / corpus-joyce-ulysses-tei
  • Started
  • 0
  • 1
  • Intermediate
  • HTML
open-editions / corpus-joyce-ulysses-tei
open-editions / corpus-joyce-ulysses-tei
  • Open
  • 0
  • 0
  • Intermediate
  • HTML
open-editions / corpus-joyce-ulysses-tei
open-editions / corpus-joyce-ulysses-tei
  • Open
  • 0
  • 0
  • Intermediate
  • HTML

Get hired!

Sign up now and apply for roles at companies that interest you.

Engineers who find a new job through WorksHub average a 15% increase in salary.

Start with GitHubStart with Stack OverflowStart with Email