Skip to content

4. Using MySQL

Mahir edited this page Jul 23, 2022 · 3 revisions

Project file structure

myapi
│--- main.go
│--- go.mod
│--- go.sum
│--- .env
│
└─── controller
│    └--- auth.go
│    └--- login.go
│    └--- user.go
│    └--- post.go
│
└─── migrate
│    └--- autoMigrate.go
│
└─── model
│    └--- auth.go
│    └--- user.go
│    └--- post.go

Part 1: Set environment variables

  • Create a project directory myapi
  • Create sub-directories controller, migrate and model inside myapi
  • Modify .env.sample file according to your database credentials and save it as .env:
APP_PORT=1200
APP_ENV=development

ACCESS_KEY=Use_a_strong_and_long_random_key
REFRESH_KEY=Use_another_strong_and_long_random_key
# expires tokens in minutes
ACCESS_KEY_TTL=5
REFRESH_KEY_TTL=60
AUDIENCE=
ISSUER=gorest
# NotBefore for ACCESS_KEY in seconds
NOT_BEFORE_ACC=0
# NotBefore for REFRESH_KEY in seconds
NOT_BEFORE_REF=0
SUBJECT=

ACTIVATE_RDBMS=yes
DBDRIVER=mysql
DBUSER=dbUser
DBPASS=dbPass
DBNAME=dbName
DBHOST=localhost
DBPORT=3306
DBSSLMODE=disable
DBTIMEZONE=your_server's_timezone

Illegal symbol: Do not use # in any environment variable.

Part 2: DB design and migration

In this tutorial, the following schema diagram is used for database design.

schema_diagram

For auths, users, and posts tables, create the respective models as structs.

myapi
└─── model
│    └--- auth.go
│    └--- user.go
│    └--- post.go

model/auth.go:

package model

import (
	"time"

	"gorm.io/gorm"
)

// Auth model - `auths` table
type Auth struct {
	AuthID    uint64 `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
	Email     string         `json:"Email"`
	Password  string         `json:"Password"`
	User      User           `gorm:"foreignkey:IDAuth;references:AuthID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}

model/user.go:

package model

import (
	"time"

	"gorm.io/gorm"
)

// User model - `users` table
type User struct {
	UserID    uint64 `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
	FirstName string         `json:"FirstName,omitempty"`
	LastName  string         `json:"LastName,omitempty"`
	IDAuth    uint64         `json:"-"`
	Posts     []Post         `gorm:"foreignkey:IDUser;references:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:",omitempty"`
}

model/post.go:

package model

import (
	"time"

	"gorm.io/gorm"
)

// Post model - `posts` table
type Post struct {
	PostID    uint64 `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
	Title     string         `json:"Title,omitempty"`
	Body      string         `json:"Body,omitempty"`
	IDUser    uint64         `json:"-"`
}

DB migration:

Collected from: https://github.com/pilinux/gorest/blob/main/database/migrate/autoMigrate.go

myapi
└─── migrate
│    └--- autoMigrate.go
// autoMigrate.go needs to be executed only when it is required

package main

import (
	"fmt"

	"gorm.io/gorm"

	"github.com/pilinux/gorest/config"
	"github.com/pilinux/gorest/database"

	"myapi/model"
)

// Load all the models
type auth model.Auth
type user model.User
type post model.Post

var db *gorm.DB
var errorState int

func main() {
	configureDB := config.Database().RDBMS
	driver := configureDB.Env.Driver

	/*
	** 0 = default/no error
	** 1 = error
	**/
	errorState = 0

	db = database.InitDB()

	// Auto migration
	/*
		- Automatically migrate schema
		- Only create tables with missing columns and missing indexes
		- Will not change/delete any existing columns and their types
	*/

	// Careful! It will drop all the tables!
	dropAllTables()

	// Automatically migrate all the tables
	migrateTables()

	// Manually set foreign keys for MySQL and PostgreSQL
	if driver != "sqlite3" {
		setPkFk()
	}

	if errorState == 0 {
		fmt.Println("Auto migration is completed!")
	} else {
		fmt.Println("Auto migration failed!")
	}
}

func dropAllTables() {
	// Careful! It will drop all the tables!
	if err := db.Migrator().DropTable(&post{}, &user{}, &auth{}); err != nil {
		errorState = 1
		fmt.Println(err)
	} else {
		fmt.Println("Old tables are deleted!")
	}
}

func migrateTables() {
	configureDB := config.Database().RDBMS
	driver := configureDB.Env.Driver

	if driver == "mysql" {
		// db.Set() --> add table suffix during auto migration
		if err := db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&auth{},
			&user{}, &post{}); err != nil {
			errorState = 1
			fmt.Println(err)
		} else {
			fmt.Println("New tables are  migrated successfully!")
		}
	} else {
		if err := db.AutoMigrate(&auth{},
			&user{}, &post{}); err != nil {
			errorState = 1
			fmt.Println(err)
		} else {
			fmt.Println("New tables are  migrated successfully!")
		}
	}
}

func setPkFk() {
	// Manually set foreign key for MySQL and PostgreSQL
	if err := db.Migrator().CreateConstraint(&auth{}, "User"); err != nil {
		errorState = 1
		fmt.Println(err)
	}

	if err := db.Migrator().CreateConstraint(&user{}, "Posts"); err != nil {
		errorState = 1
		fmt.Println(err)
	}
}

Note: The following packages are imported from gorest for database configuration.

	"github.com/pilinux/gorest/config"
	"github.com/pilinux/gorest/database"

At the root level (myapi):

go mod init myapi

go mod tidy -compat=1.17

Now, inside migrate directory, build the executable file:

cd migrate

go build

On the database server where MySQL is installed and the DB is configured, save the .env file and execute this newly built migrate file to migrate the database.

Part 3: Writing controllers

myapi
└─── controller
│    └--- auth.go
│    └--- login.go
│    └--- user.go
│    └--- post.go

Concept:

  • A new user creates a new account with an email and password
  • The user logs into the system
  • After authentication, user receives an access token and a refresh token
  • With the access token, user creates his profile (First Name & Last Name)
  • With the access token, now the user can create blogs (Title & Body)

Modify model/auth.go: hashing user password

For versions v1.5.x
package model

import (
	"encoding/json"
	"errors"
	"time"

	"gorm.io/gorm"

	"github.com/pilinux/gorest/config"
	"github.com/pilinux/gorest/lib"
)

// Auth model - `auths` table
type Auth struct {
	AuthID    uint64 `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
	Email     string         `json:"Email"`
	Password  string         `json:"Password"`
	User      User           `gorm:"foreignkey:IDAuth;references:AuthID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}

// UnmarshalJSON ...
func (v *Auth) UnmarshalJSON(b []byte) error {
	aux := struct {
		AuthID   uint64 `json:"AuthID"`
		Email    string `json:"Email"`
		Password string `json:"Password"`
	}{}
	if err := json.Unmarshal(b, &aux); err != nil {
		return err
	}

	// check password length
	// if more checks are required i.e. password pattern,
	// add all conditions here
	if len(aux.Password) < 6 {
		return errors.New("short password")
	}

	v.AuthID = aux.AuthID
	v.Email = aux.Email

	config := lib.HashPassConfig{
		Memory:      config.Security().HashPass.Memory,
		Iterations:  config.Security().HashPass.Iterations,
		Parallelism: config.Security().HashPass.Parallelism,
		SaltLength:  config.Security().HashPass.SaltLength,
		KeyLength:   config.Security().HashPass.KeyLength,
	}
	pass, err := lib.HashPass(config, aux.Password)
	if err != nil {
		return err
	}
	v.Password = pass

	return nil
}

// MarshalJSON ...
func (v Auth) MarshalJSON() ([]byte, error) {
	aux := struct {
		AuthID uint64 `json:"AuthId"`
		Email  string `json:"Email"`
	}{
		AuthID: v.AuthID,
		Email:  v.Email,
	}

	return json.Marshal(aux)
}
For versions <= v1.4.5

Note: github.com/pilinux/gorest/database/model is imported as pmodel to access HashPass function.

package model

import (
	"encoding/json"
	"errors"
	"time"

	pmodel "github.com/pilinux/gorest/database/model"

	"gorm.io/gorm"
)

// Auth model - `auths` table
type Auth struct {
	AuthID    uint64 `gorm:"primaryKey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
	Email     string         `json:"Email"`
	Password  string         `json:"Password"`
	User      User           `gorm:"foreignkey:IDAuth;references:AuthID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}

// UnmarshalJSON ...
func (v *Auth) UnmarshalJSON(b []byte) error {
	aux := struct {
		AuthID   uint64 `json:"AuthID"`
		Email    string `json:"Email"`
		Password string `json:"Password"`
	}{}
	if err := json.Unmarshal(b, &aux); err != nil {
		return err
	}

	// check password length
	// if more checks are required i.e. password pattern,
	// add all conditions here
	if len(aux.Password) < 6 {
		return errors.New("short password")
	}

	v.AuthID = aux.AuthID
	v.Email = aux.Email
	if v.Password = pmodel.HashPass(aux.Password); v.Password == "error" {
		return errors.New("HashPass failed")
	}

	return nil
}

// MarshalJSON ...
func (v Auth) MarshalJSON() ([]byte, error) {
	aux := struct {
		AuthID uint64 `json:"AuthId"`
		Email  string `json:"Email"`
	}{
		AuthID: v.AuthID,
		Email:  v.Email,
	}

	return json.Marshal(aux)
}

controller/auth.go:

Collected from: https://github.com/pilinux/gorest/blob/main/controller/auth.go

  1. [HTTP POST] http://localhost:1200/api/v1/register

Create a new user account

{
    "Email":"[email protected]",
    "Password":"... ..."
}

Note:

  • github.com/pilinux/gorest/database: connect to the MySQL database
  • github.com/pilinux/gorest/lib: ValidateEmail - check if the email format is valid (v1.5.x)
  • github.com/pilinux/gorest/service: IsEmailValid - check if the email format is valid (<= v1.4.5)
package controller

import (
	"net/http"

	"github.com/pilinux/gorest/database"
	"github.com/pilinux/gorest/lib"
	"github.com/pilinux/gorest/lib/renderer"

	"github.com/gin-gonic/gin"

	"myapi/model"
)

// CreateUserAuth - POST /register
func CreateUserAuth(c *gin.Context) {
	db := database.GetDB()
	auth := model.Auth{}
	authFinal := model.Auth{}

	// bind JSON
	if err := c.ShouldBindJSON(&auth); err != nil {
		renderer.Render(c, gin.H{"msg": "bad request"}, http.StatusBadRequest)
		return
	}

	// email validation
	if !lib.ValidateEmail(auth.Email) {
		renderer.Render(c, gin.H{"msg": "wrong email address"}, http.StatusBadRequest)
		return
	}

	// email must be unique
	if err := db.Where("email = ?", auth.Email).First(&auth).Error; err == nil {
		renderer.Render(c, gin.H{"msg": "email already registered"}, http.StatusForbidden)
		return
	}

	// user must not be able to manipulate all fields
	authFinal.Email = auth.Email
	authFinal.Password = auth.Password

	// one unique email for each account
	tx := db.Begin()
	if err := tx.Create(&authFinal).Error; err != nil {
		tx.Rollback()
		renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
	} else {
		tx.Commit()
		renderer.Render(c, authFinal, http.StatusCreated)
	}
}

controller/login.go:

Collected from: https://github.com/pilinux/gorest/blob/main/controller/login.go

Tasks:

  1. [HTTP POST] http://localhost:1200/api/v1/login

Verify email-password and issue new access and refresh tokens

{
    "Email":"[email protected]",
    "Password":"..."
}
  1. [HTTP POST] http://localhost:1200/api/v1/refresh

Verify existing refresh token and issue new access and refresh tokens

{
    "RefreshJWT":"use_existing_valid_refresh_token"
}

Note: github.com/pilinux/gorest/lib/middleware to handle JWT authorization

package controller

import (
	"net/http"

	"github.com/pilinux/gorest/lib"
	"github.com/pilinux/gorest/lib/middleware"
	"github.com/pilinux/gorest/lib/renderer"
	"github.com/pilinux/gorest/service"

	"github.com/alexedwards/argon2id"
	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
)

// LoginPayload ...
type LoginPayload struct {
	Email    string `json:"Email"`
	Password string `json:"Password"`
}

// Login ...
func Login(c *gin.Context) {
	var payload LoginPayload
	if err := c.ShouldBindJSON(&payload); err != nil {
		renderer.Render(c, gin.H{"msg": "bad request"}, http.StatusBadRequest)
		return
	}

	if !lib.ValidateEmail(payload.Email) {
		renderer.Render(c, gin.H{"msg": "wrong email address"}, http.StatusBadRequest)
		return
	}

	v, err := service.GetUserByEmail(payload.Email)
	if err != nil {
		renderer.Render(c, gin.H{"msg": "not found"}, http.StatusNotFound)
		return
	}

	verifyPass, err := argon2id.ComparePasswordAndHash(payload.Password, v.Password)
	if err != nil {
		log.WithError(err).Error("error code: 1011")
		renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
		return
	}
	if !verifyPass {
		renderer.Render(c, gin.H{"msg": "wrong credentials"}, http.StatusUnauthorized)
		return
	}

	accessJWT, _, err := middleware.GetJWT(v.AuthID, v.Email, "", "", "", "", "", "access")
	if err != nil {
		log.WithError(err).Error("error code: 1012")
		renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
		return
	}
	refreshJWT, _, err := middleware.GetJWT(v.AuthID, v.Email, "", "", "", "", "", "refresh")
	if err != nil {
		log.WithError(err).Error("error code: 1013")
		renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
		return
	}

	jwtPayload := middleware.JWTPayload{}
	jwtPayload.AccessJWT = accessJWT
	jwtPayload.RefreshJWT = refreshJWT
	renderer.Render(c, jwtPayload, http.StatusOK)
}

// Refresh ...
func Refresh(c *gin.Context) {
	authID := middleware.AuthID
	email := middleware.Email

	// check validity
	if authID == 0 {
		renderer.Render(c, gin.H{"msg": "access denied"}, http.StatusUnauthorized)
		return
	}
	if email == "" {
		renderer.Render(c, gin.H{"msg": "access denied"}, http.StatusUnauthorized)
		return
	}

	// issue new tokens
	accessJWT, _, err := middleware.GetJWT(authID, email, "", "", "", "", "", "access")
	if err != nil {
		log.WithError(err).Error("error code: 1021")
		renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
		return
	}
	refreshJWT, _, err := middleware.GetJWT(authID, email, "", "", "", "", "", "refresh")
	if err != nil {
		log.WithError(err).Error("error code: 1022")
		renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
		return
	}

	jwtPayload := middleware.JWTPayload{}
	jwtPayload.AccessJWT = accessJWT
	jwtPayload.RefreshJWT = refreshJWT
	renderer.Render(c, jwtPayload, http.StatusOK)
}

controller/user.go:

Collected from: https://github.com/pilinux/gorest/blob/main/controller/user.go

Tasks:

  1. [HTTP GET] http://localhost:1200/api/v1/users/:id

Fetch user details (first & last name) along with all blog posts belonged to the user

  1. [HTTP POST] http://localhost:1200/api/v1/users

Create a new user profile [requires JWT for verification]

{
    "FirstName": "...",
    "LastName": "..."
}
  1. [HTTP PUT] http://localhost:1200/api/v1/users

Update personal user profile [requires JWT for verification]

{
    "FirstName": "...",
    "LastName": "..."
}

Note:

  • github.com/pilinux/gorest/database: connect to the MySQL database
  • github.com/pilinux/gorest/lib/middleware: to handle JWT authorization
package controller

import (
	"net/http"
	"time"

	"github.com/pilinux/gorest/database"
	"github.com/pilinux/gorest/lib/middleware"
	"github.com/pilinux/gorest/lib/renderer"

	"github.com/gin-gonic/gin"

	"myapi/model"
)

// GetUser - GET /users/:id
func GetUser(c *gin.Context) {
	db := database.GetDB()
	id := c.Params.ByName("id")
	user := model.User{}
	posts := []model.Post{}

	if err := db.Where("user_id = ? ", id).First(&user).Error; err != nil {
		renderer.Render(c, gin.H{"msg": "not found"}, http.StatusNotFound)
	} else {
		db.Model(&posts).Where("id_user = ?", id).Find(&posts)
		user.Posts = posts
		renderer.Render(c, user, http.StatusOK)
	}
}

// CreateUser - POST /users
func CreateUser(c *gin.Context) {
	db := database.GetDB()
	user := model.User{}
	userFinal := model.User{}

	userIDAuth := middleware.AuthID

	// does the user have an existing profile
	if err := db.Where("id_auth = ?", userIDAuth).First(&userFinal).Error; err == nil {
		renderer.Render(c, gin.H{"msg": "user profile found, no need to create a new one"}, http.StatusForbidden)
		return
	}

	// bind JSON
	if err := c.ShouldBindJSON(&user); err != nil {
		renderer.Render(c, gin.H{"msg": "bad request"}, http.StatusBadRequest)
		return
	}

	// user must not be able to manipulate all fields
	userFinal.FirstName = user.FirstName
	userFinal.LastName = user.LastName
	userFinal.IDAuth = userIDAuth

	tx := db.Begin()
	if err := tx.Create(&userFinal).Error; err != nil {
		tx.Rollback()
		renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
	} else {
		tx.Commit()
		renderer.Render(c, userFinal, http.StatusCreated)
	}
}

// UpdateUser - PUT /users
func UpdateUser(c *gin.Context) {
	db := database.GetDB()
	user := model.User{}
	userFinal := model.User{}

	userIDAuth := middleware.AuthID

	// does the user have an existing profile
	if err := db.Where("id_auth = ?", userIDAuth).First(&userFinal).Error; err != nil {
		renderer.Render(c, gin.H{"msg": "no user profile found"}, http.StatusNotFound)
		return
	}

	// bind JSON
	if err := c.ShouldBindJSON(&user); err != nil {
		renderer.Render(c, gin.H{"msg": "bad request"}, http.StatusBadRequest)
		return
	}

	// user must not be able to manipulate all fields
	userFinal.UpdatedAt = time.Now()
	userFinal.FirstName = user.FirstName
	userFinal.LastName = user.LastName

	tx := db.Begin()
	if err := tx.Save(&userFinal).Error; err != nil {
		tx.Rollback()
		renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
	} else {
		tx.Commit()
		renderer.Render(c, userFinal, http.StatusOK)
	}
}

controller/post.go:

Collected from: https://github.com/pilinux/gorest/blob/main/controller/post.go

Tasks:

  1. [HTTP GET] http://localhost:1200/api/v1/posts

Fetch all blog posts saved in the database

  1. [HTTP GET] http://localhost:1200/api/v1/posts/:id

Fetch a post by its ID

  1. [HTTP POST] http://localhost:1200/api/v1/posts

Post a new blog article [requires JWT for verification]

{
    "Title": "...",
    "Body": "... ..."
}

Note:

  • github.com/pilinux/gorest/database: connect to the MySQL database
  • github.com/pilinux/gorest/lib/middleware: to handle JWT authorization
package controller

import (
	"net/http"

	"github.com/pilinux/gorest/database"
	"github.com/pilinux/gorest/lib/middleware"
	"github.com/pilinux/gorest/lib/renderer"

	"github.com/gin-gonic/gin"

	"myapi/model"
)

// GetPosts - GET /posts
func GetPosts(c *gin.Context) {
	db := database.GetDB()
	posts := []model.Post{}

	if err := db.Find(&posts).Error; err != nil {
		renderer.Render(c, gin.H{"msg": "not found"}, http.StatusNotFound)
	} else {
		renderer.Render(c, posts, http.StatusOK)
	}
}

// GetPost - GET /posts/:id
func GetPost(c *gin.Context) {
	db := database.GetDB()
	post := model.Post{}
	id := c.Params.ByName("id")

	if err := db.Where("post_id = ? ", id).First(&post).Error; err != nil {
		renderer.Render(c, gin.H{"msg": "not found"}, http.StatusNotFound)
	} else {
		renderer.Render(c, post, http.StatusOK)
	}
}

// CreatePost - POST /posts
func CreatePost(c *gin.Context) {
	db := database.GetDB()
	user := model.User{}
	post := model.Post{}
	postFinal := model.Post{}

	userIDAuth := middleware.AuthID

	// does the user have an existing profile
	if err := db.Where("id_auth = ?", userIDAuth).First(&user).Error; err != nil {
		renderer.Render(c, gin.H{"msg": "no user profile found"}, http.StatusForbidden)
		return
	}

	// bind JSON
	if err := c.ShouldBindJSON(&post); err != nil {
		renderer.Render(c, gin.H{"msg": "bad request"}, http.StatusBadRequest)
		return
	}

	// user must not be able to manipulate all fields
	postFinal.Title = post.Title
	postFinal.Body = post.Body
	postFinal.IDUser = user.UserID

	tx := db.Begin()
	if err := tx.Create(&postFinal).Error; err != nil {
		tx.Rollback()
		renderer.Render(c, gin.H{"msg": "internal server error"}, http.StatusInternalServerError)
	} else {
		tx.Commit()
		renderer.Render(c, postFinal, http.StatusCreated)
	}
}

Part 4: Writing routers

main.go:

Collected from: https://github.com/pilinux/gorest/blob/main/main.go

package main

import (
	"fmt"

	"myapi/controller"

	"github.com/gin-gonic/gin"
	"github.com/pilinux/gorest/config"
	"github.com/pilinux/gorest/database"
	"github.com/pilinux/gorest/lib/middleware"
)

var configure = config.Config()

func main() {
	if configure.Database.RDBMS.Activate == "yes" {
		// Initialize RDBMS client
		if err := database.InitDB().Error; err != nil {
			fmt.Println(err)
			return
		}
	}

	// JWT
	middleware.AccessKey = []byte(configure.Security.JWT.AccessKey)
	middleware.AccessKeyTTL = configure.Security.JWT.AccessKeyTTL
	middleware.RefreshKey = []byte(configure.Security.JWT.RefreshKey)
	middleware.RefreshKeyTTL = configure.Security.JWT.RefreshKeyTTL

	router, err := SetupRouter()
	if err != nil {
		fmt.Println(err)
		return
	}
	err = router.Run(":" + configure.Server.ServerPort)
	if err != nil {
		fmt.Println(err)
		return
	}
}

// SetupRouter ...
func SetupRouter() (*gin.Engine, error) {
	if configure.Server.ServerEnv == "production" {
		gin.SetMode(gin.ReleaseMode)
	}

	router := gin.Default()

	router.Use(middleware.CORS(
		configure.Security.CORS.Origin,
		configure.Security.CORS.Credentials,
		configure.Security.CORS.Headers,
		configure.Security.CORS.Methods,
		configure.Security.CORS.MaxAge,
	))
	// For gorest <= v1.4.5
	// router.Use(middleware.CORS())

	// API:v1
	v1 := router.Group("/api/v1/")
	{
		// RDBMS
		if configure.Database.RDBMS.Activate == "yes" {
			// Register - no JWT required
			v1.POST("register", controller.CreateUserAuth)

			// Login - app issues JWT
			v1.POST("login", controller.Login)

			// Refresh - app issues new JWT
			rJWT := v1.Group("refresh")
			rJWT.Use(middleware.RefreshJWT())
			rJWT.POST("", controller.Refresh)

			// User
			rUsers := v1.Group("users")
			rUsers.GET("/:id", controller.GetUser) // Non-protected
			rUsers.Use(middleware.JWT())
			rUsers.POST("", controller.CreateUser) // Protected
			rUsers.PUT("", controller.UpdateUser)

			// Post
			rPosts := v1.Group("posts")
			rPosts.GET("", controller.GetPosts)    // Non-protected
			rPosts.GET("/:id", controller.GetPost) // Non-protected
			rPosts.Use(middleware.JWT())
			rPosts.POST("", controller.CreatePost) // Protected
		}
	}

	return router, nil
}

Part 5: Launch the app

go mod tidy

go build

./myapi