DEV Community

Cover image for Gin Blog App Part 1
Shivam Pratap Singh
Shivam Pratap Singh

Posted on

Gin Blog App Part 1

Hey, I'm Shivam, I develop WebApps using NodeJs, Golang and sometimes Django too. Here I'm writing about how to use Gin and res-api-framework in Golang. Gin in popular golang frame work because of it's built-in fuctionality.

  • What This Part Will Cover:
  • Introduction to the series and what the final app will do
  • Project setup (Go modules, folder structure)
  • Installing Gin and other necessary packages
  • Setting up SQLite with GORM
  • Creating models (User, maybe Post as a preview)
  • Implementing user signup and login with password hashing
  • Basic testing using Postman or curl

Introduction

In this series I will explain everyting from setup to implementation, for coding part I will post in 3 parts and for explaination of code it will be a single and last part. this is a simple blog app where user can post, update and delete their post.

Project setup

create a folder stucture like this:

gin-blog-api/

├── go.mod
├── go.sum
├── main.go

├── .env

├── config/
│ └── db.go

├── middleware/
│ └── middleware.go

├── models/
│ └── user.go

├── handlers/
│ └── auth.go

├── routes/
│ └── routes.go

├── utils/
│ └── jwt.go

initialize go module

go mod init gin-blog-api
Enter fullscreen mode Exit fullscreen mode

Installing Gin and other necessary packages

go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/sqlite
go get golang.org/x/crypto/bcrypt
Enter fullscreen mode Exit fullscreen mode

Setting up SQLite with GORM

add followig in config/database.go

package config

import (
    "gin-blog-api/models"
    "log"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

var DB *gorm.DB

func ConnectDatabase() {
    database, err := gorm.Open(sqlite.Open("blog.db"), &gorm.Config{})
    if err != nil {
        log.Fatal("Failed to connect to database!", err)
    }

    err = database.AutoMigrate(&models.Post{})
    if err != nil {
        log.Fatal("Migration failed!", err)
    }

    database.AutoMigrate(&models.Post{}, &models.User{})

    DB = database
}
Enter fullscreen mode Exit fullscreen mode

Creating models

add following in models/users.go:

type User struct {
    ID       uint   `gorm:"primaryKey"`
    Username string `gorm:"unique"`
    Email    string `gorm:"unique"`
    Password string `gorm:"not null"`
}
Enter fullscreen mode Exit fullscreen mode

Implementing user signup and login with password hashing

add this in handlers/auth.go

package handlers

import (
    "fmt"
    "gin-blog-api/config"
    "gin-blog-api/models"
    "net/http"

    "gin-blog-api/utils"

    "github.com/gin-gonic/gin"
    "golang.org/x/crypto/bcrypt"
)

func Signup(c *gin.Context) {
    var user models.User

    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    fmt.Println("RAW password received:", user.Password)

    if user.Password == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Password is required"})
        return
    }

    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not hash password"})
        return
    }
    user.Password = string(hashedPassword)

    if err := config.DB.Create(&user).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create user"})
        return
    }

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

func Login(c *gin.Context) {
    var loginData struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    var user models.User

    if err := c.ShouldBindJSON(&loginData); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    if err := config.DB.Where("email = ?", loginData.Email).First(&user).Error; err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
        return
    }

    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginData.Password)); err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"})
        return
    }

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

    c.SetCookie("token", token, 24*3600, "/", "localhost", false, true)

    c.JSON(http.StatusOK, gin.H{
        "token": token,
        "user": gin.H{
            "id":       user.ID,
            "username": user.Username,
            "email":    user.Email,
        },
    })

}

func Logout(c *gin.Context) {
    // For stateless JWT authentication, logout is handled on the client side.
    // Here we can just return a success message.
    c.SetCookie("token", "", -1, "/", "localhost", false, true) // Clear the cookie
    c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
}
Enter fullscreen mode Exit fullscreen mode

creating JWT token

utils/jwt.go

package utils

import (
    "fmt"
    "log"
    "os"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

func GenerateJWT(userID uint) (string, error) {

    claims := jwt.MapClaims{
        "user_id": userID,
        "exp":     time.Now().Add(24 * time.Hour).Unix(), // 1 day expiry
    }

    log.Println("JWT_SECRET during sign:", os.Getenv("JWT_SECRET"))
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    return token.SignedString([]byte(os.Getenv("JWT_SECRET")))
}

func ParseJWT(tokenStr string) (uint, error) {

    token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
        return []byte(os.Getenv("JWT_SECRET")), nil
    })

    if err != nil {
        log.Println("JWT parse error:", err)
        return 0, fmt.Errorf("invalid or expired token")
    }

    if !token.Valid {
        log.Println("JWT invalid token")
        return 0, fmt.Errorf("invalid or expired token")
    }

    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok {
        log.Println("JWT claims cast failed")
        return 0, fmt.Errorf("invalid token claims")
    }

    uidFloat, ok := claims["user_id"].(float64)
    if !ok {
        log.Println("user_id missing in claims")
        return 0, fmt.Errorf("user_id not found in token")
    }
    log.Println("Parsed user_id:", uidFloat)

    // save the user_id in context for later use
    if uidFloat <= 0 {
        log.Println("Invalid user_id in claims:", uidFloat)
        return 0, fmt.Errorf("invalid user_id in token")
    }

    return uint(uidFloat), nil
}
Enter fullscreen mode Exit fullscreen mode

Main server file

/main.go

package main

import (
    "gin-blog-api/config"
    "gin-blog-api/routes"

    "github.com/gin-gonic/gin"
    "github.com/joho/godotenv"
    "log"
)

func init() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }
}

func main() {
    r := gin.Default()
    // Connect to the database
    config.ConnectDatabase()
    // Register routes
    routes.RegisterRoutes(r)
    // Start the server
    // Listen and serve on
    // http://localhost:8080

    r.Run(":8080")
}

Enter fullscreen mode Exit fullscreen mode

Routing

routes/routes.go

package routes

import (
    "gin-blog-api/handlers"
    "gin-blog-api/middleware"

    "github.com/gin-gonic/gin"
)

func RegisterRoutes(router *gin.Engine) {
    router.POST("/signup", handlers.Signup)
    router.POST("/login", handlers.Login)

    router.GET("/posts", handlers.GetPosts)
    router.GET("/posts/:id", handlers.GetPostByID)

    auth := router.Group("/")
    auth.Use(middleware.JWTAuthMiddleware())
    {
        auth.POST("/posts", handlers.CreatePost)
        auth.PUT("/posts/:id", handlers.UpdatePost)
        auth.DELETE("/posts/:id", handlers.DeletePost)
        router.POST("/logout", handlers.Logout)

    }
}

Enter fullscreen mode Exit fullscreen mode

middleware to protect routes

middleware/auth.go

package middlewares

import (
    "gin-blog-api/utils"
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
)

func JWTAuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing or invalid"})
            return
        }

        tokenString := strings.TrimPrefix(authHeader, "Bearer ")

        userID, err := utils.ParseJWT(tokenString)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
            return
        }

        // Attach user ID to context
        c.Set("userID", userID)
        c.Next()
    }
}

Enter fullscreen mode Exit fullscreen mode

add .env

JWT_SECRET=9ad4404902df5dde56b33bb7857e085b24b1903187d4a1b3d7c1b1526d4da4dba7de59dacb68424f52325c1e606b700653eab5c722da394d60b3d81c9de503a8
Enter fullscreen mode Exit fullscreen mode

Basic testing using Postman or curl

http://localhost:8080/signup/
{
  "username":"codey-singh",
  "email":"ssp@yopmail.com",
  "password":"qwert1234"
}
response: 
{
  "message": "User created successfully"
}


http://localhost:8080/login/
{
  "email":"ssp@yopmail.com",
  "password":"qwert1234"
}
response:
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJleHAiOjE3NDg0NDk0ODEsInVzZXJfaWQiOjM3MjAwMX0.
hgI8nM5RrnnAkpbOfgM4cd0hdIXI9tCVfQAnAn5ahcc",
  "user": {
    "email": "ssp@yopmail.com",
    "id": 372001,
    "username": "codey-singh"
  }
}


http://localhost:8080/protected/
add: bearer + token in auth postman
response:
{
  "message": "Protected content",
  "user": 372001
}
Enter fullscreen mode Exit fullscreen mode

Run command

go run main.go
Enter fullscreen mode Exit fullscreen mode




what is in next part:

In next part i will setup a post model and functionality
In third part i will setup a user dashboard
and in final part i will proivde full code explaination
full github code will be public in third part.

Top comments (0)