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
}
Make sure this model is auto-migrated:
db.AutoMigrate(&models.Post{})
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)
}
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)
}
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)
}
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"})
}
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)
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)