DEV Community

loading...
Cover image for GO Backend Service Skeleton w/Gin

GO Backend Service Skeleton w/Gin

mrthkc profile image mrthkc ・6 min read

Intro

Starting this post I assume that you have basic installation of GoLang on your local environment. If not you can follow steps from; Download and Install - The Go Programming Language

Go Directory

After download and install steps, your go(path) directory should be like below;

Go Path

You need to create your awesome project's folder under src/ directory. If you are using GitHub to host your codes, it would be better to start project under path like;
~/go/src/github.com/{github-username}/{project-name}. That makes easier using go commands such as go get, go install etc.

Go Project Directory View

Now We are ready to open our preferred Go IDE to form project directory layout and code. Here you are my layout;

Layout

I will try to explain every file in this directory layout from top to bottom and you can start your backend project by making changes specific to you.

Cmd -/api

Under cmd we can keep starting the system scripts. In this example you can see api/main.go to start our local backend service. Also, you can store fetchers, data importers or fixing scripts under this directory. To run;
go run cmd/api/main.go, specifically for this sample.

Let's see main.go;

package main

import (
    "flag"

    "github.com/{github-username}/{project}/internal/api"
    "github.com/{github-username}/{project}/internal/pkg/conf"
    log "github.com/sirupsen/logrus"
)

func main() {
    env := flag.String("env", "local", "environment")
    flag.Parse()

    config, err := conf.NewConfig("config/default.yml", *env)
    if err != nil {
        log.Fatalf("Can not reead config: %v", err)
    }

    config.Env = *env
    config.DBCred = config.Mysql.Local
    config.Secret = config.JWT.Local.Secret
    if config.Env == "prod" {
        config.DBCred = config.Mysql.Prod
        config.Secret = config.JWT.Prod.Secret
    }

    log.Info("SDK API started w/Env: " + *env)
    api.Listen(config)
}
Enter fullscreen mode Exit fullscreen mode

As you can see basically we are reading service's configuration and starting the API by using Listen function under api package.

Config -/default.yml

As you can guess, under this directory we are storing our configurations and credentials. For secrets we can think another location but basically it is OK to keep them here.

Also yml file is a personal decision, you can select your favorite format such as json, xml, etc.

mysql:
  local:
    host: localhost
    port: 3306
    db: project
    user: root
    password: 12345678
  prod:
    host: prod_db
    port: 3306
    db: project
    user: user
    password: strong_pass

jwt:
  local:
    secret: "test"
  prod:
    secret: "prod"

Enter fullscreen mode Exit fullscreen mode

Internal

Under internal directory we store our service's base scripts. I divided the code base mainly two parts;

  1. service (api) - it could be fetcher, importer, etc.
  2. pkg - structs and functions that we will use for services.
Api (Service)

As I mentioned main functions for an API backend service locating here. Yes, main listen function, cors header function and router for this example;

base.go: by using configuration starting gin

// Listen : starts api by listening to incoming requests
func Listen(c *conf.Config) {
    service.Config = c

    if service.Config.Env == "prod" {
        gin.SetMode(gin.ReleaseMode)
    }
    r := gin.New()
    r.Use(CORS())

    // router.go
    route(r)

    server := &http.Server{
        Addr:         ":9090",
        Handler:      r,
        ReadTimeout:  60 * time.Second,
        WriteTimeout: 60 * time.Second,
    }

    err := server.ListenAndServe()
    if err != nil && err != http.ErrServerClosed {
        log.Fatalf("Server can not start: %v", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

CORS function that we use to set our response headers.
You can change them specifically by looking how you request and collect responses from backend service.

// CORS : basic cors settings
func CORS() 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, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        c.Next()
    }
}
Enter fullscreen mode Exit fullscreen mode

router.go: Classical router.
Important parts:

  • endpoint to function match
  • using middleware(JWTAuth()) and grouping endpoints.
func route(r *gin.Engine) {
    // Base
    r.GET("/api/", service.BaseHandler)

    // Register
    r.POST("/api/user", service.Register)
    r.POST("/api/login", service.Login)

    authorized := r.Group("/api/")
    authorized.Use(service.JWTAuth())
    {
        authorized.GET("token", service.Token)
        authorized.GET("user/:uid/profile", service.Profile)
    }
}
Enter fullscreen mode Exit fullscreen mode
Pkg

We store our service's deep and detailed parts in here.

Adapters:

In my project, I am using MySQL DB only but by looking your project's requirements you can multiple adapters like a NoSQL which needs to establish a connection.

mysql.go:

func NewClient(host string, port string, dbName string, uName string, pass string) (db *sql.DB) {
    db, err := sql.Open("mysql", uName+":"+pass+"@tcp("+host+":"+port+")/"+dbName)
    if err != nil {
        panic(err.Error())
    }
    return db
}
Enter fullscreen mode Exit fullscreen mode
Configuration Reader:

To read our configuration file into a struct:
conf/reader.go:

type Config struct {
    Env   string
    Mysql struct {
        Local struct {
            Host     string `yaml:"host"`
            Port     string `yaml:"port"`
            DB       string `yaml:"db"`
            User     string `yaml:"user"`
            Password string `yaml:"password"`
        } `yaml:"local"`
        Prod struct {
            Host     string `yaml:"host"`
            Port     string `yaml:"port"`
            DB       string `yaml:"db"`
            User     string `yaml:"user"`
            Password string `yaml:"password"`
        } `yaml:"prod"`
    } `yaml:"mysql"`
    JWT struct {
        Local struct {
            Secret     string `yaml:"secret"`
        } `yaml:"local"`
        Prod struct {
            Secret     string `yaml:"secret"`
        } `yaml:"prod"`
    } `yaml:"jwt"`

    DBCred struct {
        Host     string `yaml:"host"`
        Port     string `yaml:"port"`
        DB       string `yaml:"db"`
        User     string `yaml:"user"`
        Password string `yaml:"password"`
    }
    Secret string
}

// NewConfig returns a new decoded Config struct
func NewConfig(configPath string, env string) (*Config, error) {
    // Create config structure
    config := &Config{}

    // Open config file
    file, err := ioutil.ReadFile(configPath)
    if err != nil {
        return nil, err
    }

    // Init new YAML decode
    err = yaml.Unmarshal(file, config)
    if err != nil {
        return nil, err
    }

    return config, nil
}
Enter fullscreen mode Exit fullscreen mode
Entity:

Under entity directory, first of all under entity/main.go I preferred to keep a DB variable for usage of all possible entities.

// DB : DB for all entities
var DB *sql.DB
Enter fullscreen mode Exit fullscreen mode

And as you can guess, in entity/user.go we are using this DB variable for SELECT/INSERT/UPDATE querying functions. Also a map struct for DB user schema to make easier to read rows.

user.go:

// User : user entity
type User struct {
    ID          int        `json:"id"`
    Email       string     `json:"email"`
    Password    string     `json:"password"`
    Fullname    string     `json:"fullname"`
    TSLastLogin int32      `json:"ts_last_login"`
    TSCreate    int32      `json:"ts_create"`
    TSUpdate    int32      `json:"ts_update"`
    Permission  json.RawMessage `json:"permission"`
}

// GetUserByEmail : returns single user
func GetUserByEmail(email string) *User {
    user := new(User)
    row := DB.QueryRow("SELECT * from user WHERE email=?", email)
    err := row.Scan(&user.ID, &user.Email, &user.Password, &user.Fullname, &user.TSLastLogin, &user.TSCreate, &user.TSUpdate, &user.Permission)
    if err != nil {
        log.Errorln("User SELECT by Email Err: ", err)
        return nil
    }
    return user
}
Enter fullscreen mode Exit fullscreen mode
Service:

Under service directory there are handlers / middleware functions. These functions are the base point to respond requests coming from any external service.

Under service/base.go, we can add a baseHandler to check health of the service.

// BaseHandler : home - health-test
func BaseHandler(c *gin.Context) {
    log.Info("Base")
}
Enter fullscreen mode Exit fullscreen mode

Under service/jwt.go, you can find the middleware function to validate and auth token, that we called from router.go as it is mentioned.

// ValidateToken :
func ValidateToken(encodedToken string) (*jwt.Token, error) {
    return jwt.Parse(encodedToken, func(token *jwt.Token) (interface{}, error) {
        if _, isvalid := token.Method.(*jwt.SigningMethodHMAC); !isvalid {
            return nil, errors.New("invalid token")
        }
        return []byte(Config.Secret), nil
    })
}

// JWTAuth :
func JWTAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        var bearer = "Bearer"
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }

        tokenString := authHeader[len(bearer):]
        token, err := ValidateToken(tokenString)
        if token.Valid {
            claims := token.Claims.(jwt.MapClaims)
            log.Debug(claims)
        } else {
            log.Error(err.Error())
            c.AbortWithStatus(http.StatusUnauthorized)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And lastly, service/user.go, there are functions related to user schema. Such as login, register, profile, etc. Here you are login function that we used in router.go to respond api/login endpoint.

// Login :
func Login(c *gin.Context) {
    entity.DB = adapter.NewClient(Config.DBCred.Host, Config.DBCred.Port, Config.DBCred.DB, Config.DBCred.User, Config.DBCred.Password)
    defer entity.DB.Close()

    user := new(entity.User)
    if err := c.BindJSON(user); err != nil {
        log.Error("Binding user error occured: ", err)
        c.JSON(http.StatusBadRequest, "Binding error")
        return
    }

    checkUser := entity.GetUserByEmail(user.Email)
    if checkUser == nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "success": 0,
        })
        return
    }

    login := comparePasswords(checkUser.Password, []byte(user.Password))
    if login == false {
        c.JSON(http.StatusForbidden, gin.H{
            "success": 0,
        })
        return
    }

    token, err := GenerateToken(checkUser.ID)
    if err != nil {
        c.JSON(http.StatusForbidden, gin.H{
            "success": 0,
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "success": 1,
        "data":    gin.H{
            "uid": checkUser.ID,
            "uemail": checkUser.Email,
            "token": token,
        },
    })
    return
}
Enter fullscreen mode Exit fullscreen mode

As a conclusion, I am using this layout while starting any side project's backend service. By adding new entities, service functions and endpoints, it is really sufficient to save your time that is spent in starting step.

Cheers.

Discussion (2)

Collapse
zdev1official profile image
ZDev1Official

Pretty awesome 😁

Collapse
mrthkc profile image
Forem Open with the Forem app