20 KiB
Utility Controllers Guide
This guide explains the new utility controllers that make development easier, simpler, and better.
Table of Contents
- Response Helper
- Pagination Helper
- Query Helper
- Validation Helper
- Audit Log Controller
- Batch Operations Controller
- Export Helper
- 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
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
{
"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
// 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
// GET /api/v1/articles?sort=created_at:desc
query := controllers.QueryParser.ApplySortFromContext(
c, db.Model(&models.Article{}), "created_at", "desc"
)
Basic Search
// GET /api/v1/articles?search=football
query := controllers.QueryParser.ApplySearchFromContext(
c, db.Model(&models.Article{}), "title", "content"
)
Fluent Chain Builder (Recommended)
// 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
searchorq: Search termsort: 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
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
// 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
if !controllers.Validator.IsValidEmail(email) {
controllers.Respond.BadRequest(c, "Invalid email")
return
}
Validation Tags
required: Field is requiredemail: Valid email addressurl: Valid URLmin=n: Minimum lengthmax=n: Maximum lengthslug: Valid slug (lowercase, alphanumeric, hyphens)color: Valid hex color (#RGB, #RRGGBB)oneof=val1 val2: One of the specified valuesgte=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:
// Initialize audit logger
controllers.InitAuditLogger(dbInstance)
// Add to AutoMigrate
&models.AuditLog{},
Usage
Log Actions
// 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)
// 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
// Initialize in main.go or controller
controllers.InitBatchOperations(dbInstance)
Usage
Batch Delete
// 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
// 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
// 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
// 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
{
"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
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
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
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
// 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)
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)
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
// In main.go AutoMigrate
&models.AuditLog{},
Step 2: Initialize in main.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
go get github.com/go-playground/validator/v10
Best Practices
- Always use standardized responses - Use
Respond.*methods - Log important actions - Use
AuditLoggerfor CREATE, UPDATE, DELETE - Validate inputs - Use
Validator.ValidateAndRespond - Sanitize user input - Use
Validator.Sanitize*methods - Use query chains - Use
QueryParser.BuildQueryChainfor complex queries - Paginate large lists - Always use
Paginatorfor list endpoints - Batch operations - Use
BatchOpsfor bulk actions - Export functionality - Use
Exporterfor CSV/JSON exports
Testing
# 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