help@rskworld.in +91 93305 39277
RSK World
  • Home
  • Development
    • Web Development
    • Mobile Apps
    • Software
    • Games
    • Project
  • Technologies
    • Data Science
    • AI Development
    • Cloud Development
    • Blockchain
    • Cyber Security
    • Dev Tools
    • Testing Tools
  • Blog
  • About
  • Contact

Theme Settings

Color Scheme
Display Options
Font Size
100%
Back to Project
RSK World
go-rest-api
/
internal
/
handlers
RSK World
go-rest-api
Go REST API - Enterprise-grade REST API with JWT Authentication + PostgreSQL + Redis Caching + Docker + Comprehensive Testing + Educational Design
handlers
  • api_handler.go1.4 KB
  • auth_handler.go4.4 KB
  • auth_handler_test.go4.6 KB
  • category_handler.go5.2 KB
  • product_handler.go8.8 KB
  • upload_handler.go5 KB
CustomFormulaBuilder.storyboardroutes.goproduct_handler.go
internal/routes/routes.go
Raw Download
Find: Go to:
/*
* Author: RSK World
* Email: help@rskworld.in / support@rskworld.in
* Website: https://rskworld.in/contact.php
* Year: 2026
*/

package routes

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/rskworld/go-rest-api/internal/config"
	"github.com/rskworld/go-rest-api/internal/handlers"
	"github.com/rskworld/go-rest-api/internal/middleware"

	swaggerFiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
)

func SetupRouter(r *gin.Engine, cfg *config.Config) {
	// Global Middleware
	r.Use(gin.Recovery())
	r.Use(middleware.LoggerMiddleware())
	r.Use(middleware.CORSMiddleware())
	r.Use(middleware.RateLimitMiddleware())

	// Handlers
	authHandler := handlers.NewAuthHandler(cfg)
	apiHandler := handlers.NewAPIHandler()
	productHandler := handlers.NewProductHandler()
	categoryHandler := handlers.NewCategoryHandler()
	uploadHandler := handlers.NewUploadHandler(cfg)

	// Swagger configuration
	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

	// Static file serving for uploads
	r.Static("/uploads", cfg.UploadPath)

	// API Versioning - V1 Routes
	v1 := r.Group("/api/v1")
	setupV1Routes(v1, cfg, authHandler, apiHandler, productHandler, categoryHandler, uploadHandler)

	// Legacy routes (redirect to v1 for backward compatibility)
	api := r.Group("/api")
	setupLegacyRoutes(api, cfg, authHandler, apiHandler, productHandler, categoryHandler, uploadHandler)
}

// V1 API Routes
func setupV1Routes(rg *gin.RouterGroup, cfg *config.Config, authHandler *handlers.AuthHandler, apiHandler *handlers.APIHandler, productHandler *handlers.ProductHandler, categoryHandler *handlers.CategoryHandler, uploadHandler *handlers.UploadHandler) {
	// Public Routes
	{
		rg.POST("/register", authHandler.Register)
		rg.POST("/login", authHandler.Login)
		rg.GET("/health", func(c *gin.Context) {
			c.JSON(200, gin.H{
				"status":  "ok",
				"message": "Service is healthy",
				"version": "v1",
				"author":  "RSK World",
			})
		})

		// Category Routes (Public for View - read-only)
		rg.GET("/categories", categoryHandler.GetCategories)
		rg.GET("/categories/:id", categoryHandler.GetCategory)

		// Product Routes (Public for View - read-only)
		rg.GET("/products", productHandler.GetProducts)
		rg.GET("/products/:id", productHandler.GetProduct)
	}

	// Protected Routes
	protected := rg.Group("/")
	protected.Use(middleware.AuthMiddleware(cfg))
	{
		protected.GET("/profile", apiHandler.GetUserProfile)

		// Admin-only Routes (Write operations)
		admin := protected.Group("/")
		admin.Use(middleware.RequireAdmin())
		{
			// Category management
			admin.POST("/categories", categoryHandler.CreateCategory)
			admin.PUT("/categories/:id", categoryHandler.UpdateCategory)
			admin.DELETE("/categories/:id", categoryHandler.DeleteCategory)

			// Product management
			admin.POST("/products", productHandler.CreateProduct)
			admin.PUT("/products/:id", productHandler.UpdateProduct)
			admin.DELETE("/products/:id", productHandler.DeleteProduct)

			// File upload management
			admin.POST("/products/:id/upload", uploadHandler.UploadProductImage)
			admin.DELETE("/products/:id/image", uploadHandler.DeleteProductImage)
		}
	}
}

// Legacy API Routes (for backward compatibility)
func setupLegacyRoutes(rg *gin.RouterGroup, cfg *config.Config, authHandler *handlers.AuthHandler, apiHandler *handlers.APIHandler, productHandler *handlers.ProductHandler, categoryHandler *handlers.CategoryHandler, uploadHandler *handlers.UploadHandler) {
	// Redirect legacy routes to v1
	rg.POST("/register", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/register") })
	rg.POST("/login", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/login") })
	rg.GET("/health", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/health") })

	// Category Routes
	rg.GET("/categories", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/categories") })
	rg.GET("/categories/:id", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/categories/"+c.Param("id")) })

	// Product Routes
	rg.GET("/products", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/products") })
	rg.GET("/products/:id", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/products/"+c.Param("id")) })

	// Protected routes - these need proper middleware setup
	protected := rg.Group("/")
	protected.Use(middleware.AuthMiddleware(cfg))
	{
		protected.GET("/profile", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/profile") })

		// Admin routes
		admin := protected.Group("/")
		admin.Use(middleware.RequireAdmin())
		{
			admin.POST("/categories", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/categories") })
			admin.PUT("/categories/:id", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/categories/"+c.Param("id")) })
			admin.DELETE("/categories/:id", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/categories/"+c.Param("id")) })

			admin.POST("/products", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/products") })
			admin.PUT("/products/:id", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/products/"+c.Param("id")) })
			admin.DELETE("/products/:id", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/products/"+c.Param("id")) })

			admin.POST("/products/:id/upload", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/products/"+c.Param("id")+"/upload") })
			admin.DELETE("/products/:id/image", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/api/v1/products/"+c.Param("id")+"/image") })
		}
	}
}
140 lines•5.8 KB
go
internal/handlers/product_handler.go
Raw Download
Find: Go to:
/*
* Author: RSK World
* Email: help@rskworld.in / support@rskworld.in
* Website: https://rskworld.in/contact.php
* Year: 2026
*/

package handlers

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/rskworld/go-rest-api/internal/cache"
	"github.com/rskworld/go-rest-api/internal/database"
	"github.com/rskworld/go-rest-api/internal/models"
	"github.com/rskworld/go-rest-api/internal/response"
	"github.com/rskworld/go-rest-api/internal/validation"
)

type ProductHandler struct{}

func NewProductHandler() *ProductHandler {
	return &ProductHandler{}
}

// CreateProduct godoc
// @Summary Create a new product
// @Description Create a new product with the input payload
// @Tags products
// @Accept  json
// @Produce  json
// @Param product body models.Product true "Create product"
// @Success 201 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 500 {object} response.Response
// @Security BearerAuth
// @Router /products [post]
func (h *ProductHandler) CreateProduct(c *gin.Context) {
	var product models.Product
	if err := c.ShouldBindJSON(&product); err != nil {
		response.ValidationError(c, err.Error())
		return
	}

	// Validate the product data
	if err := validation.ValidateStruct(product); err != nil {
		response.ValidationError(c, err.Error())
		return
	}

	if result := database.DB.Create(&product); result.Error != nil {
		response.InternalServerError(c, "Failed to create product")
		return
	}

	// Invalidate cache
	if cache.RedisClient != nil {
		cache.RedisClient.FlushAll() // Simple approach - invalidate all product caches
	}

	response.Success(c, http.StatusCreated, "Product created successfully", product)
}

// GetProducts godoc
// @Summary Get list of products
// @Description Get list of products with pagination, search and filtering
// @Tags products
// @Accept  json
// @Produce  json
// @Param page query int false "Page number"
// @Param limit query int false "Number of items per page"
// @Param search query string false "Search term for product name or description"
// @Param category_id query int false "Filter by category ID"
// @Param min_price query number false "Minimum price filter"
// @Param max_price query number false "Maximum price filter"
// @Param in_stock query bool false "Filter only in-stock products"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /products [get]
func (h *ProductHandler) GetProducts(c *gin.Context) {
	var products []models.Product
	var totalCount int64

	// Check cache for simple queries (no search/filter)
	cacheKey := ""
	useCache := true

	search := c.Query("search")
	categoryID := c.Query("category_id")
	minPrice := c.Query("min_price")
	maxPrice := c.Query("max_price")
	inStock := c.Query("in_stock")

	if search != "" || categoryID != "" || minPrice != "" || maxPrice != "" || inStock != "" {
		useCache = false
	} else {
		page := c.DefaultQuery("page", "1")
		limit := c.DefaultQuery("limit", "10")
		cacheKey = fmt.Sprintf("%s:page:%s:limit:%s", cache.ProductsKey, page, limit)

		// Try to get from cache
		if cache.RedisClient != nil && cache.RedisClient.Exists(cacheKey) {
			var cachedResponse gin.H
			if err := cache.RedisClient.Get(cacheKey, &cachedResponse); err == nil {
				response.Success(c, http.StatusOK, "Products retrieved from cache", cachedResponse)
				return
			}
		}
	}

	// Build query
	query := database.DB.Model(&models.Product{}).Preload("Category")

	// Search functionality
	if search != "" {
		searchTerm := "%" + strings.ToLower(search) + "%"
		query = query.Where("LOWER(name) LIKE ? OR LOWER(description) LIKE ?", searchTerm, searchTerm)
	}

	// Category filter
	if categoryID != "" {
		query = query.Where("category_id = ?", categoryID)
	}

	// Price range filters
	if minPrice != "" {
		if price, err := strconv.ParseFloat(minPrice, 64); err == nil {
			query = query.Where("price >= ?", price)
		}
	}

	if maxPrice != "" {
		if price, err := strconv.ParseFloat(maxPrice, 64); err == nil {
			query = query.Where("price <= ?", price)
		}
	}

	// In stock filter
	if inStock == "true" {
		query = query.Where("stock > 0")
	}

	// Get total count for pagination
	query.Count(&totalCount)

	// Pagination
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
	offset := (page - 1) * limit

	// Apply pagination and execute query
	if result := query.Limit(limit).Offset(offset).Find(&products); result.Error != nil {
		response.InternalServerError(c, "Failed to fetch products")
		return
	}

	// Calculate total pages
	totalPages := (totalCount + int64(limit) - 1) / int64(limit)

	result := gin.H{
		"data":        products,
		"page":        page,
		"limit":       limit,
		"total_count": totalCount,
		"total_pages": totalPages,
	}

	// Cache the result if it's a simple query
	if useCache && cacheKey != "" && cache.RedisClient != nil {
		cache.RedisClient.Set(cacheKey, result, cache.ListCacheDuration)
	}

	response.Success(c, http.StatusOK, "Products retrieved successfully", result)
}

// GetProduct godoc
// @Summary Get a product by ID
// @Description Get details of a specific product by ID
// @Tags products
// @Accept  json
// @Produce  json
// @Param id path int true "Product ID"
// @Success 200 {object} response.Response
// @Failure 404 {object} response.Response
// @Router /products/{id} [get]
func (h *ProductHandler) GetProduct(c *gin.Context) {
	id := c.Param("id")
	cacheKey := fmt.Sprintf("%s%s", cache.ProductKeyPrefix, id)

	// Try to get from cache first
	var product models.Product
	if cache.RedisClient != nil && cache.RedisClient.Exists(cacheKey) {
		if err := cache.RedisClient.Get(cacheKey, &product); err == nil {
			response.Success(c, http.StatusOK, "Product retrieved from cache", product)
			return
		}
	}

	// Get from database
	if result := database.DB.Preload("Category").First(&product, id); result.Error != nil {
		response.NotFound(c, "Product")
		return
	}

	// Cache the result
	if cache.RedisClient != nil {
		cache.RedisClient.Set(cacheKey, product, cache.ProductCacheDuration)
	}

	response.Success(c, http.StatusOK, "Product retrieved successfully", product)
}

// UpdateProduct godoc
// @Summary Update a product
// @Description Update a product by ID
// @Tags products
// @Accept  json
// @Produce  json
// @Param id path int true "Product ID"
// @Param product body models.Product true "Update product"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.Response
// @Failure 404 {object} response.Response
// @Security BearerAuth
// @Router /products/{id} [put]
func (h *ProductHandler) UpdateProduct(c *gin.Context) {
	id := c.Param("id")
	var product models.Product

	if result := database.DB.First(&product, id); result.Error != nil {
		response.NotFound(c, "Product")
		return
	}

	var updateData models.Product
	if err := c.ShouldBindJSON(&updateData); err != nil {
		response.ValidationError(c, err.Error())
		return
	}

	// Validate the update data
	if err := validation.ValidateStruct(updateData); err != nil {
		response.ValidationError(c, err.Error())
		return
	}

	// Update only non-zero fields
	if err := database.DB.Model(&product).Updates(updateData).Error; err != nil {
		response.InternalServerError(c, "Failed to update product")
		return
	}

	// Reload the updated product
	if err := database.DB.First(&product, id).Error; err != nil {
		response.InternalServerError(c, "Failed to retrieve updated product")
		return
	}

	// Invalidate cache
	if cache.RedisClient != nil {
		productCacheKey := fmt.Sprintf("%s%s", cache.ProductKeyPrefix, id)
		cache.RedisClient.Delete(productCacheKey)
		cache.RedisClient.FlushAll() // Invalidate list caches
	}

	response.Success(c, http.StatusOK, "Product updated successfully", product)
}

// DeleteProduct godoc
// @Summary Delete a product
// @Description Delete a product by ID
// @Tags products
// @Accept  json
// @Produce  json
// @Param id path int true "Product ID"
// @Success 200 {object} response.Response
// @Failure 500 {object} response.Response
// @Security BearerAuth
// @Router /products/{id} [delete]
func (h *ProductHandler) DeleteProduct(c *gin.Context) {
	id := c.Param("id")
	if result := database.DB.Delete(&models.Product{}, id); result.Error != nil {
		response.InternalServerError(c, "Failed to delete product")
		return
	}

	// Invalidate cache
	if cache.RedisClient != nil {
		productCacheKey := fmt.Sprintf("%s%s", cache.ProductKeyPrefix, id)
		cache.RedisClient.Delete(productCacheKey)
		cache.RedisClient.FlushAll() // Invalidate list caches
	}

	response.Success(c, http.StatusOK, "Product deleted successfully", nil)
}

301 lines•8.8 KB
go

About RSK World

Founded by Molla Samser, with Designer & Tester Rima Khatun, RSK World is your one-stop destination for free programming resources, source code, and development tools.

Founder: Molla Samser
Designer & Tester: Rima Khatun

Development

  • Game Development
  • Web Development
  • Mobile Development
  • AI Development
  • Development Tools

Legal

  • Terms & Conditions
  • Privacy Policy
  • Disclaimer

Contact Info

Nutanhat, Mongolkote
Purba Burdwan, West Bengal
India, 713147

+91 93305 39277

hello@rskworld.in
support@rskworld.in

© 2026 RSK World. All rights reserved.

Content used for educational purposes only. View Disclaimer