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;
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;
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)
}
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"
Internal
Under internal directory we store our service's base scripts. I divided the code base mainly two parts;
- service (api) - it could be fetcher, importer, etc.
- 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)
}
}
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()
}
}
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)
}
}
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
}
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
}
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
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
}
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")
}
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)
}
}
}
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
}
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.
Top comments (2)
Pretty awesome 😁
Thanks