DEV Community

Shivam Pratap Singh
Shivam Pratap Singh

Posted on

Gin Blog App Part 2

Introduction

In Part 1, we built user authentication and JWT middleware. Now in Part 2, we’ll implement CRUD operations for blog posts, each protected by JWT authentication. Only the post owner can update/delete their post.

Database Model for Blog Posts

models/post.go

package models

type Post struct {
    ID        uint   `json:"id" gorm:"primaryKey"`
    Title     string `json:"title"`
    Content   string `json:"content"`
    UserID    uint   `json:"user_id"`
    CreatedAt time.Time
}

Enter fullscreen mode Exit fullscreen mode

Make sure this model is auto-migrated:

db.AutoMigrate(&models.Post{})
Enter fullscreen mode Exit fullscreen mode

Create Blog Post

func CreatePost(c *gin.Context) {
    var post models.Post

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

    // Check if user exists
    var user models.User
    if err := config.DB.First(&user, post.UserID).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }

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

    c.JSON(http.StatusCreated, post)
}

Enter fullscreen mode Exit fullscreen mode

Get All Posts & Single Post


type PostResponse struct {
    ID      uint       `json:"id"`
    Title   string     `json:"title"`
    Content string     `json:"content"`
    User    SimpleUser `json:"user"`
}

// SimpleUser represents a simplified user structure for responses
type SimpleUser struct {
    ID       uint   `json:"id"`
    Username string `json:"username"`
}

func GetPosts(c *gin.Context) {
    var posts []models.Post
    if err := config.DB.Preload("User").Find(&posts).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch posts"})
        return
    }

    // PostResponse represents the response structure for a post with user info

    var responsePosts []PostResponse
    for _, post := range posts {
        responsePosts = append(responsePosts, PostResponse{
            ID:      uint(post.ID),
            Title:   post.Title,
            Content: post.Content,
            User: SimpleUser{
                ID:       post.User.ID,
                Username: post.User.Username,
            },
        })
    }

    c.JSON(http.StatusOK, responsePosts)
}

func GetPost(c *gin.Context) {
    idParam := c.Param("id")
    if idParam == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "ID parameter is required"})
        return
    }

    id, err := strconv.Atoi(idParam)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
        return
    }

    var post models.Post
    if err := config.DB.Preload("User").First(&post, id).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
        return
    }

    response := PostResponse{
        ID:      uint(post.ID),
        Title:   post.Title,
        Content: post.Content,
        User: SimpleUser{
            ID:       post.User.ID,
            Username: post.User.Username,
        },
    }

    c.JSON(http.StatusOK, response)
}

Enter fullscreen mode Exit fullscreen mode

Update Blog Post

Only the owner can update the post:

func UpdatePost(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil || id <= 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
        return
    }

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

    var post models.Post
    if err := config.DB.First(&post, id).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
        return
    }

    if post.UserID != updatedPost.UserID {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "You are not the author of this post"})
        return
    }

    // Update the fields
    post.Title = updatedPost.Title
    post.Content = updatedPost.Content

    if err := config.DB.Save(&post).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update post"})
        return
    }

    c.JSON(http.StatusOK, post)
}

Enter fullscreen mode Exit fullscreen mode

Delete Blog Post

func DeletePost(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil || id <= 0 {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
        return
    }

    var payload struct {
        UserID uint `json:"user_id"`
    }
    if err := c.ShouldBindJSON(&payload); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    var post models.Post
    if err := config.DB.First(&post, id).Error; err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
        return
    }

    if post.UserID != payload.UserID {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "You are not the author of this post"})
        return
    }

    if err := config.DB.Delete(&post).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete post"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "Post deleted"})
}
Enter fullscreen mode Exit fullscreen mode

Protecting Routes with JWT

Update your routes/routes.go:

auth := router.Group("/api")
auth.Use(middleware.JWTAuthMiddleware())

auth.POST("/posts", handlers.CreatePost)
auth.PUT("/posts/:id", handlers.UpdatePost)
auth.DELETE("/posts/:id", handlers.DeletePost)

// Public routes
router.GET("/posts", handlers.GetPosts)
router.GET("/posts/:id", handlers.GetPost)
Enter fullscreen mode Exit fullscreen mode

Testing the Endpoints

Use Postman or ThunderClient:

POST /api/posts → Add Bearer Token or Cookie
GET /posts → Public
PUT/DELETE /api/posts/:id → Must be owner

Next Part Preview

In Part 3

Search & filter by title/user
Pagination
User-specific blog dashboard
Deployment tips

Follow the journey on Dev.to, Medium, and X. and X -> dont spam here i'm doin fun.

Top comments (0)