# Utility Controllers Guide This guide explains the new utility controllers that make development easier, simpler, and better. ## Table of Contents 1. [Response Helper](#response-helper) 2. [Pagination Helper](#pagination-helper) 3. [Query Helper](#query-helper) 4. [Validation Helper](#validation-helper) 5. [Audit Log Controller](#audit-log-controller) 6. [Batch Operations Controller](#batch-operations-controller) 7. [Export Helper](#export-helper) 8. [Complete Examples](#complete-examples) --- ## Response Helper **Purpose:** Standardize all API responses across your application. ### Features - Consistent response format - Pre-built status code handlers - Support for metadata (pagination, etc.) - Easy error handling ### Usage ```go import "fotbal-club/internal/controllers" // Success response controllers.Respond.Success(c, data, "Operation successful") // Success with metadata (pagination) controllers.Respond.SuccessWithMeta(c, articles, paginationMeta, "Articles retrieved") // Created (201) controllers.Respond.Created(c, newArticle, "Article created") // Error responses controllers.Respond.BadRequest(c, "Invalid input") controllers.Respond.Unauthorized(c, "Not authenticated") controllers.Respond.Forbidden(c, "No permission") controllers.Respond.NotFound(c, "Article not found") controllers.Respond.InternalError(c, "Database error") controllers.Respond.ValidationError(c, validationErrors) // No content (204) controllers.Respond.NoContent(c) ``` ### Response Format ```json { "success": true, "message": "Articles retrieved successfully", "data": [...], "meta": { "page": 1, "page_size": 20, "total": 100, "total_pages": 5, "has_next": true, "has_prev": false } } ``` --- ## Pagination Helper **Purpose:** Add pagination to any list endpoint with one line of code. ### Features - Auto-extracts page/page_size from query params - Calculates pagination metadata - Works with GORM queries - Supports preloading ### Usage ```go // Simple pagination query := db.Model(&models.Article{}).Where("published = ?", true) var articles []models.Article meta, err := controllers.Paginator.Paginate(c, query, &articles) if err != nil { controllers.Respond.InternalError(c, "Failed to retrieve articles") return } controllers.Respond.SuccessWithMeta(c, articles, meta, "Success") // Pagination with preloading meta, err := controllers.Paginator.PaginateWithPreload( c, query, &articles, "Author", "Category" ) ``` ### Query Parameters ``` GET /api/v1/articles?page=1&page_size=20 ``` - `page`: Page number (default: 1) - `page_size`: Items per page (default: 20, max: 100) --- ## Query Helper **Purpose:** Simplify filtering, sorting, and searching in list endpoints. ### Features - Search across multiple fields - Sort by any field - Boolean filters - ID filters (comma-separated) - Date range filters - Fluent chain builder ### Usage #### Basic Sorting ```go // GET /api/v1/articles?sort=created_at:desc query := controllers.QueryParser.ApplySortFromContext( c, db.Model(&models.Article{}), "created_at", "desc" ) ``` #### Basic Search ```go // GET /api/v1/articles?search=football query := controllers.QueryParser.ApplySearchFromContext( c, db.Model(&models.Article{}), "title", "content" ) ``` #### Fluent Chain Builder (Recommended) ```go // GET /api/v1/articles?search=football&sort=created_at:desc&published=true&category_ids=1,2,3&from=2024-01-01 query := controllers.QueryParser.BuildQueryChain(c, db.Model(&models.Article{})). WithSearch("title", "content"). WithSort("created_at", "desc"). WithBoolFilter("published", "published"). WithBoolFilter("featured", "featured"). WithIDsFilter("category_ids", "category_id"). WithDateRange("created_at"). Build() var articles []models.Article meta, err := controllers.Paginator.Paginate(c, query, &articles) ``` ### Supported Query Parameters - `search` or `q`: Search term - `sort`: field:order (e.g., `created_at:desc`) - `published`: Boolean filter (true/false) - `featured`: Boolean filter (true/false) - `category_ids`: Comma-separated IDs (e.g., `1,2,3`) - `from`: Start date (YYYY-MM-DD) - `to`: End date (YYYY-MM-DD) --- ## Validation Helper **Purpose:** Validate request data and return user-friendly error messages. ### Features - Struct validation using tags - Custom validators (slug, color) - Automatic error responses - Input sanitization ### Usage #### Validate Struct ```go type CreateArticleRequest struct { Title string `validate:"required,min=3,max=200"` Content string `validate:"required,min=10"` Slug string `validate:"omitempty,slug"` Email string `validate:"required,email"` } var req CreateArticleRequest if err := c.ShouldBindJSON(&req); err != nil { controllers.Respond.BadRequest(c, "Invalid JSON") return } // Validate and auto-respond if invalid if !controllers.Validator.ValidateAndRespond(c, req) { return // Response already sent } // Continue with valid data... ``` #### Sanitization ```go // Sanitize string (trim, normalize spaces) title := controllers.Validator.SanitizeString(req.Title) // Sanitize email (lowercase, trim) email := controllers.Validator.SanitizeEmail(req.Email) // Sanitize slug slug := controllers.Validator.SanitizeSlug(req.Slug) ``` #### Individual Validation ```go if !controllers.Validator.IsValidEmail(email) { controllers.Respond.BadRequest(c, "Invalid email") return } ``` ### Validation Tags - `required`: Field is required - `email`: Valid email address - `url`: Valid URL - `min=n`: Minimum length - `max=n`: Maximum length - `slug`: Valid slug (lowercase, alphanumeric, hyphens) - `color`: Valid hex color (#RGB, #RRGGBB) - `oneof=val1 val2`: One of the specified values - `gte=n`, `lte=n`: Greater/less than or equal --- ## Audit Log Controller **Purpose:** Track all important actions in your application for compliance and debugging. ### Features - Automatic user tracking - IP address and user agent logging - Before/after change tracking - Search and filter logs - Statistics dashboard ### Setup Add to `main.go`: ```go // Initialize audit logger controllers.InitAuditLogger(dbInstance) // Add to AutoMigrate &models.AuditLog{}, ``` ### Usage #### Log Actions ```go // Log creation controllers.AuditLogger.LogCreate(c, "Article", article.ID, "Article created: "+article.Title) // Log update with changes before := map[string]interface{}{"title": oldTitle, "published": oldPublished} after := map[string]interface{}{"title": newTitle, "published": newPublished} controllers.AuditLogger.LogUpdate(c, "Article", article.ID, "Article updated", before, after) // Log deletion controllers.AuditLogger.LogDelete(c, "Article", articleID, "Article deleted: "+title) // Log login controllers.AuditLogger.LogLogin(c, userID, true) // success=true // Log custom action controllers.AuditLogger.LogEntry(c, "EXPORT", "Article", nil, "Exported articles to CSV", nil) ``` #### API Endpoints (Admin Only) ```go // Add to routes/routes.go admin group: admin.GET("/audit-logs", auditLogController.GetAuditLogs) admin.GET("/audit-logs/:id", auditLogController.GetAuditLogByID) admin.GET("/audit-logs/entity/:entity_type/:entity_id", auditLogController.GetEntityAuditHistory) admin.GET("/audit-logs/user/:user_id", auditLogController.GetUserActivityLog) admin.GET("/audit-logs/stats", auditLogController.GetAuditStats) admin.POST("/audit-logs/cleanup", auditLogController.CleanupOldLogs) ``` #### Query Audit Logs ``` GET /api/v1/admin/audit-logs?action=CREATE&entity_type=Article&user_id=1&from=2024-01-01&page=1 ``` --- ## Batch Operations Controller **Purpose:** Perform bulk operations efficiently. ### Features - Batch delete - Batch update - Batch publish/unpublish - Batch reorder - Detailed success/failure reporting ### Setup ```go // Initialize in main.go or controller controllers.InitBatchOperations(dbInstance) ``` ### Usage #### Batch Delete ```go // POST /api/v1/articles/batch-delete // Body: {"ids": [1, 2, 3, 4, 5]} func (ac *ArticleController) BatchDeleteArticles(c *gin.Context) { controllers.BatchOps.BatchDelete(c, &models.Article{}, "articles") } ``` #### Batch Update ```go // POST /api/v1/articles/batch-update // Body: {"ids": [1, 2, 3], "fields": {"published": true, "featured": false}} func (ac *ArticleController) BatchUpdateArticles(c *gin.Context) { allowedFields := []string{"published", "featured", "category_id"} controllers.BatchOps.BatchUpdate(c, &models.Article{}, "articles", allowedFields) } ``` #### Batch Publish/Unpublish ```go // POST /api/v1/articles/batch-publish // Body: {"ids": [1, 2, 3]} func (ac *ArticleController) BatchPublishArticles(c *gin.Context) { controllers.BatchOps.BatchPublish(c, &models.Article{}, "articles", true) } ``` #### Batch Reorder ```go // POST /api/v1/navigation/batch-reorder // Body: {"orders": [{"id": 1, "order": 3}, {"id": 2, "order": 1}]} func (nc *NavigationController) BatchReorderItems(c *gin.Context) { controllers.BatchOps.BatchReorder(c, &models.NavigationItem{}, "navigation_items") } ``` ### Response Format ```json { "success": true, "data": { "success": true, "total_items": 5, "success_count": 5, "failure_count": 0, "errors": [] }, "message": "Successfully deleted 5 items" } ``` --- ## Export Helper **Purpose:** Export data to CSV or JSON format. ### Usage #### Export to CSV ```go func (ac *ArticleController) ExportArticlesToCSV(c *gin.Context) { var articles []models.Article if err := db.Find(&articles).Error; err != nil { controllers.Respond.InternalError(c, "Failed to retrieve articles") return } headers := []string{"ID", "Title", "Published", "Created At"} filename := fmt.Sprintf("articles_%s.csv", time.Now().Format("20060102")) if err := controllers.Exporter.ExportToCSV(c, articles, filename, headers); err != nil { controllers.Respond.InternalError(c, "Export failed") return } } ``` #### Export to JSON ```go func (ac *ArticleController) ExportArticlesToJSON(c *gin.Context) { var articles []models.Article if err := db.Find(&articles).Error; err != nil { controllers.Respond.InternalError(c, "Failed to retrieve articles") return } filename := fmt.Sprintf("articles_%s.json", time.Now().Format("20060102")) if err := controllers.Exporter.ExportToJSON(c, articles, filename); err != nil { controllers.Respond.InternalError(c, "Export failed") return } } ``` --- ## Complete Examples ### Example 1: Simple CRUD Controller ```go package controllers import ( "fotbal-club/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type SimpleArticleController struct { DB *gorm.DB } // List articles with search, filter, sort, and pagination func (sac *SimpleArticleController) List(c *gin.Context) { query := QueryParser.BuildQueryChain(c, sac.DB.Model(&models.Article{})). WithSearch("title", "content"). WithSort("created_at", "desc"). WithBoolFilter("published", "published"). Build() var articles []models.Article meta, err := Paginator.PaginateWithPreload(c, query, &articles, "Author", "Category") if err != nil { Respond.InternalError(c, "Failed to retrieve articles") return } Respond.SuccessWithMeta(c, articles, meta, "Articles retrieved successfully") } // Get single article func (sac *SimpleArticleController) Get(c *gin.Context) { id := c.Param("id") var article models.Article if err := sac.DB.Preload("Author").Preload("Category").First(&article, id).Error; err != nil { if err == gorm.ErrRecordNotFound { Respond.NotFound(c, "Article not found") return } Respond.InternalError(c, "Failed to retrieve article") return } Respond.Success(c, article, "Article retrieved successfully") } // Create article func (sac *SimpleArticleController) Create(c *gin.Context) { var req struct { Title string `json:"title" validate:"required,min=3,max=200"` Content string `json:"content" validate:"required,min=10"` Slug string `json:"slug" validate:"omitempty,slug"` } if err := c.ShouldBindJSON(&req); err != nil { Respond.BadRequest(c, "Invalid JSON") return } if !Validator.ValidateAndRespond(c, req) { return } article := models.Article{ Title: Validator.SanitizeString(req.Title), Content: req.Content, Slug: Validator.SanitizeSlug(req.Slug), } if err := sac.DB.Create(&article).Error; err != nil { Respond.InternalError(c, "Failed to create article") return } AuditLogger.LogCreate(c, "Article", article.ID, "Article created: "+article.Title) Respond.Created(c, article, "Article created successfully") } // Update article func (sac *SimpleArticleController) Update(c *gin.Context) { id := c.Param("id") var article models.Article if err := sac.DB.First(&article, id).Error; err != nil { if err == gorm.ErrRecordNotFound { Respond.NotFound(c, "Article not found") return } Respond.InternalError(c, "Failed to retrieve article") return } oldTitle := article.Title var req struct { Title string `json:"title" validate:"omitempty,min=3,max=200"` } if err := c.ShouldBindJSON(&req); err != nil { Respond.BadRequest(c, "Invalid JSON") return } if !Validator.ValidateAndRespond(c, req) { return } if req.Title != "" { article.Title = Validator.SanitizeString(req.Title) } if err := sac.DB.Save(&article).Error; err != nil { Respond.InternalError(c, "Failed to update article") return } AuditLogger.LogUpdate(c, "Article", article.ID, "Article updated", map[string]interface{}{"title": oldTitle}, map[string]interface{}{"title": article.Title}) Respond.Success(c, article, "Article updated successfully") } // Delete article func (sac *SimpleArticleController) Delete(c *gin.Context) { id := c.Param("id") var article models.Article if err := sac.DB.First(&article, id).Error; err != nil { if err == gorm.ErrRecordNotFound { Respond.NotFound(c, "Article not found") return } Respond.InternalError(c, "Failed to retrieve article") return } title := article.Title articleID := article.ID if err := sac.DB.Delete(&article).Error; err != nil { Respond.InternalError(c, "Failed to delete article") return } AuditLogger.LogDelete(c, "Article", articleID, "Article deleted: "+title) Respond.NoContent(c) } ``` ### Example 2: Route Registration ```go // In routes/routes.go func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) { // Initialize helpers controllers.InitAuditLogger(db) controllers.InitBatchOperations(db) // Article routes with new utilities articles := api.Group("/articles") { articleCtrl := &controllers.SimpleArticleController{DB: db} // Public routes articles.GET("", articleCtrl.List) articles.GET("/:id", articleCtrl.Get) // Protected routes protected := articles.Group("") protected.Use(middleware.JWTAuth(db)) { protected.POST("", articleCtrl.Create) protected.PUT("/:id", articleCtrl.Update) protected.DELETE("/:id", articleCtrl.Delete) // Batch operations protected.POST("/batch-delete", articleCtrl.BatchDelete) protected.POST("/batch-publish", articleCtrl.BatchPublish) // Export protected.GET("/export/csv", articleCtrl.ExportCSV) protected.GET("/export/json", articleCtrl.ExportJSON) } } // Audit logs (admin only) auditLogCtrl := controllers.NewAuditLogController(db) admin := api.Group("/admin") admin.Use(middleware.JWTAuth(db), middleware.RoleAuth("admin")) { admin.GET("/audit-logs", auditLogCtrl.GetAuditLogs) admin.GET("/audit-logs/:id", auditLogCtrl.GetAuditLogByID) admin.GET("/audit-logs/stats", auditLogCtrl.GetAuditStats) admin.POST("/audit-logs/cleanup", auditLogCtrl.CleanupOldLogs) } } ``` --- ## Benefits ### Before (Old Way) ```go func GetArticles(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } offset := (page - 1) * pageSize var articles []models.Article var total int64 query := db.Model(&models.Article{}) query.Count(&total) query.Offset(offset).Limit(pageSize).Find(&articles) totalPages := int(math.Ceil(float64(total) / float64(pageSize))) c.JSON(200, gin.H{ "data": articles, "page": page, "page_size": pageSize, "total": total, "total_pages": totalPages, }) } ``` ### After (New Way) ```go func GetArticles(c *gin.Context) { query := QueryParser.BuildQueryChain(c, db.Model(&models.Article{})). WithSearch("title", "content"). WithSort("created_at", "desc"). Build() var articles []models.Article meta, _ := Paginator.Paginate(c, query, &articles) Respond.SuccessWithMeta(c, articles, meta, "Success") } ``` ### Comparison - **Lines of code:** 24 → 7 (70% reduction) - **Consistency:** Standardized across all endpoints - **Features:** Added search, sorting, filtering - **Error handling:** Built-in - **Maintainability:** Single source of truth --- ## Migration Guide ### Step 1: Add Model Migration ```go // In main.go AutoMigrate &models.AuditLog{}, ``` ### Step 2: Initialize in main.go ```go // After database initialization controllers.InitAuditLogger(dbInstance) controllers.InitBatchOperations(dbInstance) ``` ### Step 3: Update Existing Controllers Replace manual pagination, filtering, and response code with the new utilities. ### Step 4: Add go-playground/validator Dependency ```bash go get github.com/go-playground/validator/v10 ``` --- ## Best Practices 1. **Always use standardized responses** - Use `Respond.*` methods 2. **Log important actions** - Use `AuditLogger` for CREATE, UPDATE, DELETE 3. **Validate inputs** - Use `Validator.ValidateAndRespond` 4. **Sanitize user input** - Use `Validator.Sanitize*` methods 5. **Use query chains** - Use `QueryParser.BuildQueryChain` for complex queries 6. **Paginate large lists** - Always use `Paginator` for list endpoints 7. **Batch operations** - Use `BatchOps` for bulk actions 8. **Export functionality** - Use `Exporter` for CSV/JSON exports --- ## Testing ```bash # Test pagination curl "http://localhost:8080/api/v1/articles?page=1&page_size=10" # Test search curl "http://localhost:8080/api/v1/articles?search=football" # Test sorting curl "http://localhost:8080/api/v1/articles?sort=created_at:desc" # Test combined curl "http://localhost:8080/api/v1/articles?search=football&sort=created_at:desc&published=true&page=1" # Test batch delete curl -X POST "http://localhost:8080/api/v1/articles/batch-delete" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"ids": [1, 2, 3]}' # Test audit logs curl "http://localhost:8080/api/v1/admin/audit-logs?action=CREATE&entity_type=Article" ``` --- ## Conclusion These utility controllers dramatically reduce boilerplate code, improve consistency, and make your codebase more maintainable. They follow Go best practices and provide a solid foundation for rapid development. **Key Improvements:** - 70% less code for common operations - Consistent API responses - Built-in validation and sanitization - Comprehensive audit logging - Efficient batch operations - Easy data export - Better error handling - Improved developer experience