Skip to main content

Command Palette

Search for a command to run...

Building e-commerce Golang JWT part 1

Updated
β€’8 min read
Building e-commerce Golang JWT part 1

Building Modern application from e-commerce platforms to SaaS dashboardsβ€”require one fundamental building block before anything else can work: authentication.
Before a user can add items to a cart, checkout, store addresses, save preferences, or perform any meaningful action, they must be able to:

  • create an account, and

  • log into it securely.

This first installment of the E-Commerce API in Go series focuses entirely on building that secure foundation.

In this series, I am building a complete backend inspired by the roadmap.sh e-commerce project. But instead of jumping straight into products or payments, we start with something every real backend must handle properly:

πŸ” A robust, minimal, production-ready authentication system.

That means:

  • Secure user registration

  • Password hashing using bcrypt

  • Login with verification against a database

  • Issuing JWT tokens for user sessions

  • Validating tokens for protected routes

Even though the end goal is a full e-commerce platform (cart, payments, orders, and more), authentication is the backbone that everything else will rely on.

🎯 What We Build in This Part

This article focuses on:

  • Setting up MySQL using Docker

  • Creating a Users table

  • Designing a clean, layered project structure

  • Implementing registration (with hashed passwords)

  • Implementing login (with JWT token generation)

  • Writing clean controller β†’ service β†’ db code

  • Testing everything with curl

By the end of this post, you will have a working backend where users can sign up and log inβ€”using a proper, real-world authentication system suitable for production-grade projects.

🧱 1. Start MySQL Using Docker (Required)

docker run -d \
  --name mysql-ecom \
  -e MYSQL_ROOT_PASSWORD=rootpass \
  -e MYSQL_DATABASE=e-commerce \
  -e MYSQL_USER=admin \
  -e MYSQL_PASSWORD=adminpass \
  -p 3306:3306 \
  mysql:8

What this container setup does:

  • Creates a MySQL server named mysql-ecom

  • Exposes it on localhost:3306

  • Creates a database named e-commerce

  • Creates application user:

    • username: admin

    • password: adminpass

  • Stores data inside the container (fine for local use)

  • Uses MySQL 8.x official image

Now your Go backend can connect using this DSN:

admin:adminpass@tcp(localhost:3306)/e-commerce?parseTime=true

πŸ—„ 2. Users Table (SQL Migration)

internal/db/migrations/db.sql:

CREATE TABLE IF NOT EXISTS users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

What this schema represents:

  • id β€” unique identifier for users

  • username β€” must be unique

  • password_hash β€” stores bcrypt hash, never plain text

  • created_at β€” timestamp of registration

  • No email yet (kept minimal for the project requirement)

  • No roles / permissions at this stage

  • Helps keep authentication simple for beginners

πŸ— 3. Project Folder Structure

e-commerce/
β”œβ”€β”€ cmd/
β”‚   └── main.go               
β”œβ”€β”€ internal/
β”‚   β”œβ”€β”€ controller/           
β”‚   β”œβ”€β”€ service/              
β”‚   β”œβ”€β”€ db/                   
β”‚   β”œβ”€β”€ routes/               
β”‚   └── middleware/           
β”œβ”€β”€ model/
β”‚   └── model.go
β”œβ”€β”€ utils/
β”‚   └── utils.go              
└── config.env

Why this structure?

  • Separates web handlers (controller) from logic (service)

  • Keeps database queries in one place (db/)

  • Prevents massive β€œgod files”

  • Makes the code scalable for future features (cart, products, orders)

  • Matches common Go project patterns

πŸ”Œ 4. Database Connection Layer

File: internal/db/db.go

var (
    db   *sql.DB
    once sync.Once
)

How this works:

  • A global db *sql.DB connection is shared across the application

  • sync.Once ensures that DB initialization happens only once

  • Prevents accidental creation of multiple DB pools

func Init() error {
    once.Do(func() {
        db, err = sql.Open("mysql", dsn)
        if err != nil {
            return
        }
        db.SetConnMaxLifetime(time.Minute * 3)
        db.SetMaxOpenConns(10)
        db.SetMaxIdleConns(10)
        err = db.Ping()
    })
    return err
}

Important details about this initializer:

  • sql.Open configures the MySQL driver

  • Ping() ensures DB is actually reachable

  • The pool is tuned:

    • Max lifetime per connection: 3 minutes

    • Up to 10 open and 10 idle connections

  • Improves performance for an API with multiple concurrent requests

  • Runs only once during app startup

User Fetch Query

func GetUserByUsername(username string) (*User, error) {
    var user User
    err := db.QueryRow(
        "SELECT id, username, password_hash FROM users WHERE username = ?", username,
    ).Scan(&user.ID, &user.Username, &user.PasswordHash)

    return &user, err
}

What this function does:

  • Fetches a user by username

  • If no row exists β†’ Scan returns an error

  • Populates:

    • ID

    • username

    • hashed password

  • Used by both:

    • Registration (check duplicate username)

    • Login (validate credentials)

User Insert Query

func CreateUser(username, passwordHash string) (int64, error) {
    result, err := db.Exec(
        "INSERT INTO users (username, password_hash) VALUES (?, ?)",
        username, passwordHash,
    )
    return result.LastInsertId(), err
}

Why this insert is important:

  • Stores hashed password instead of plain text

  • Returns inserted user's ID

  • Uses parameterized queries (prevents SQL injection)

  • Ensures security best practices from the beginning

πŸ”§ 5. Utils β€” Hashing & JWT

utils/utils.go

Password Hashing

func GenerateHash(password []byte) ([]byte, error) {
    return bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
}

How this guarantees secure passwords:

  • bcrypt automatically generates a random salt

  • Default cost is secure enough for production

  • Returns a long cryptographic string like:
    $2a$10$kLfP...

  • Saved directly into DB

Password Comparison

func CompareHash(hashedpassword, password []byte) bool {
    return bcrypt.CompareHashAndPassword(hashedpassword, password) == nil
}

How comparison works internally:

  • bcrypt extracts its embedded salt

  • Re-hashes the incoming password

  • Performs constant-time comparison

  • Returns true only on a perfect match

Creating a JWT

func GenerateJWT(username string) (string, error) {
    claims := jwt.MapClaims{
        "username": username,
        "exp":      time.Now().Add(24 * time.Hour).Unix(),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    secret := LoadSecret()
    return token.SignedString(secret)
}

How JWT generation works:

  • JWT contains:

    • username

    • expiration timestamp

  • Signed using HS256 algorithm

  • Secret is loaded from .env (HMAC_SECRET)

  • Output is a string like:

      eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
    
  • Clients will store this and send with every request

Validating a JWT

func ValidateJWT(tokenString string) (string, error) {
    secret := LoadSecret()

    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return secret, nil
    })

    if err != nil || !token.Valid {
        return "", err
    }

    claims := token.Claims.(jwt.MapClaims)
    return claims["username"].(string), nil
}

How validation works:

  • Decodes and verifies the signature

  • Ensures the algorithm matches

  • Checks expiry time

  • Extracts username field from claims

  • Rejects expired or tampered JWTs

🧠 6. Service Layer (auth.go)

internal/service/auth.go

Registration Logic

func RegisterUser(username, password string) error {
    existing, _ := db.GetUserByUsername(username)
    if existing != nil {
        return errors.New("user already exists")
    }

    hash, err := utils.GenerateHash([]byte(password))
    if err != nil {
        return err
    }

    _, err = db.CreateUser(username, string(hash))
    return err
}

What happens here:

  • The system checks if username already exists

  • Password is converted to a bcrypt hash

  • Hash replaces prior plain text password

  • Stored in DB

  • If user already exists β†’ error returned

  • No tokens involved at this stage

Login Logic

func AuthenticateUser(username, password string) (bool, error) {
    user, err := db.GetUserByUsername(username)
    if err != nil {
        return false, errors.New("user not found")
    }

    if !utils.CompareHash([]byte(user.PasswordHash), []byte(password)) {
        return false, errors.New("invalid password")
    }
    return true, nil
}

Detailed flow:

  • Fetch username from DB

  • If user not found β†’ return error

  • Compare stored bcrypt hash with given plain password

  • If match β†’ authentication successful

  • If mismatch β†’ invalid password

  • No JWT generated here; just validation

🌐 7. Controller Layer (HTTP Endpoints)

internal/controller/controller.go

Registration Endpoint

func RegisterHandler(c *gin.Context) {
    var users model.User
    if err := c.ShouldBindJSON(&users); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request format"})
        return
    }

    err := service.RegisterUser(users.Username, users.Password)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "User registered successfully"})
}

What this handler does:

  • Reads the incoming JSON body into model.User

  • Validates input format automatically via ShouldBindJSON

  • Calls service layer to handle the actual registration logic

  • Responds with success message on successful registration

  • Converts Go errors into HTTP error responses

  • Keeps controller lightweight and clean

Login Endpoint

func LoginHandler(c *gin.Context) {
    var users model.User
    if err := c.ShouldBindJSON(&users); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Bad request format"})
        return
    }

    ok, err := service.AuthenticateUser(users.Username, users.Password)
    if !ok {
        c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
        return
    }

    token, err := utils.GenerateJWT(users.Username)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"token": token})
}

Detailed behavior:

  • Binds incoming JSON

  • Calls service layer to verify credentials

  • If authentication fails β†’ 401 Unauthorized

  • If authentication succeeds:

    • JWT token is generated

    • Token includes expiration + username

    • Returned to user in JSON response

  • Token can now be used to access protected APIs in future parts

πŸ›£ 8. Route Setup

internal/routes/route.go

func SetupRoutes(r *gin.Engine) {
    r.POST("/signup", controller.RegisterHandler)
    r.POST("/login", controller.LoginHandler)
}

How routes are organized:

  • /signup handles registration

  • /login handles authentication

  • Only POST methods accepted

  • These routes will later be grouped with protected routes like /cart, /profile, etc.

πŸš€ 9. Start the Go Server

Inside project root:

go run cmd/main.go

Backend will start on:

http://localhost:8080

πŸ§ͺ 10. Testing Using curl

Test Registration

curl -X POST http://localhost:8080/signup \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","password":"mypassword"}'

Response:

{"message":"User registered successfully"}

Test Login

curl -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","password":"mypassword"}'

Response:

{"token":"<jwt-token-here>"}

Copy this tokenβ€”future endpoints will require it in:

Authorization: Bearer <token>

πŸ”„ 11. Authentication Flow Summary

Registration Flow

Client β†’ /signup  
 controller β†’ service β†’ db  
 hash password β†’ store to MySQL  
 return success

Login Flow

Client β†’ /login  
 controller β†’ service β†’ db  
 verify password β†’ generate JWT  
 return token

Token Usage

Client sends token β†’ Authorization header  
JWT middleware checks token  
Controller gets authenticated username

🧩 Final Thoughts

In this first part of the series, we built:

  • A production-ready JWT authentication system

  • Secure bcrypt password storage

  • MySQL-backed user registration

  • A layered architecture that scales

  • Clean controller/service/db separation