turash/docs/dev_guides/01_gin_framework.md
Damir Mukimov 000eab4740
Major repository reorganization and missing backend endpoints implementation
Repository Structure:
- Move files from cluttered root directory into organized structure
- Create archive/ for archived data and scraper results
- Create bugulma/ for the complete application (frontend + backend)
- Create data/ for sample datasets and reference materials
- Create docs/ for comprehensive documentation structure
- Create scripts/ for utility scripts and API tools

Backend Implementation:
- Implement 3 missing backend endpoints identified in gap analysis:
  * GET /api/v1/organizations/{id}/matching/direct - Direct symbiosis matches
  * GET /api/v1/users/me/organizations - User organizations
  * POST /api/v1/proposals/{id}/status - Update proposal status
- Add complete proposal domain model, repository, and service layers
- Create database migration for proposals table
- Fix CLI server command registration issue

API Documentation:
- Add comprehensive proposals.md API documentation
- Update README.md with Users and Proposals API sections
- Document all request/response formats, error codes, and business rules

Code Quality:
- Follow existing Go backend architecture patterns
- Add proper error handling and validation
- Match frontend expected response schemas
- Maintain clean separation of concerns (handler -> service -> repository)
2025-11-25 06:01:16 +01:00

9.5 KiB

Gin Framework Development Guide

Library: github.com/gin-gonic/gin
Used In: MVP - HTTP API server
Purpose: HTTP web framework for building RESTful APIs


Where It's Used

  • Primary HTTP framework for API endpoints
  • Business registration, resource flow CRUD operations
  • Match retrieval endpoints
  • Authentication middleware

Official Documentation


Installation

go get github.com/gin-gonic/gin

Key Concepts

1. Basic Server Setup

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default() // Includes Logger and Recovery middleware
    
    // Or minimal setup
    // r := gin.New()
    // r.Use(gin.Logger())
    // r.Use(gin.Recovery())
    
    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "ok"})
    })
    
    r.Run(":8080")
}

2. Routes and Handlers

// GET request
r.GET("/api/businesses/:id", getBusiness)

// POST request
r.POST("/api/businesses", createBusiness)

// PUT request
r.PUT("/api/businesses/:id", updateBusiness)

// DELETE request
r.DELETE("/api/businesses/:id", deleteBusiness)

// Handler function
func getBusiness(c *gin.Context) {
    id := c.Param("id") // Get path parameter
    // ... business logic
    c.JSON(200, business)
}

3. Request Binding

// JSON binding
type BusinessRequest struct {
    Name  string `json:"name" binding:"required"`
    Email string `json:"email" binding:"required,email"`
}

func createBusiness(c *gin.Context) {
    var req BusinessRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    // ... process request
    c.JSON(201, business)
}

// Query parameters
func searchBusinesses(c *gin.Context) {
    name := c.Query("name")        // ?name=...
    page := c.DefaultQuery("page", "1") // ?page=... (default: "1")
    // ...
}

// Path parameters
func getBusiness(c *gin.Context) {
    id := c.Param("id") // /businesses/:id
    // ...
}

4. Middleware

// Global middleware
r.Use(corsMiddleware())
r.Use(authMiddleware())

// Route-specific middleware
r.GET("/protected", authMiddleware(), protectedHandler)

// Custom middleware
func authMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.JSON(401, gin.H{"error": "Unauthorized"})
            c.Abort()
            return
        }
        // Validate token
        c.Set("userID", userID) // Store in context
        c.Next() // Continue to next handler
    }
}

// Logging middleware (built-in)
r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
    return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
        param.ClientIP,
        param.TimeStamp.Format(time.RFC1123),
        param.Method,
        param.Path,
        param.Request.Proto,
        param.StatusCode,
        param.Latency,
        param.Request.UserAgent(),
        param.ErrorMessage,
    )
}))

5. Error Handling

func handleBusiness(c *gin.Context) {
    business, err := service.GetBusiness(c.Param("id"))
    if err != nil {
        if err == ErrNotFound {
            c.JSON(404, gin.H{"error": "Business not found"})
            return
        }
        c.JSON(500, gin.H{"error": "Internal server error"})
        return
    }
    c.JSON(200, business)
}

// Custom error handler
func errorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        if len(c.Errors) > 0 {
            c.JSON(500, gin.H{"errors": c.Errors})
        }
    }
}

6. Grouping Routes

api := r.Group("/api")
{
    // All routes prefixed with /api
    api.GET("/businesses", listBusinesses)
    
    // Nested groups
    v1 := api.Group("/v1")
    {
        v1.GET("/businesses", listBusinessesV1)
        v1.POST("/businesses", createBusinessV1)
    }
    
    v2 := api.Group("/v2")
    {
        v2.GET("/businesses", listBusinessesV2)
    }
}

// With middleware
authenticated := r.Group("/api")
authenticated.Use(authMiddleware())
{
    authenticated.GET("/profile", getProfile)
    authenticated.POST("/resources", createResource)
}

7. Context Usage

// Get values from context (set by middleware)
userID := c.MustGet("userID").(string)

// Set value in context
c.Set("businessID", businessID)

// Get with default
page, exists := c.Get("page")
if !exists {
    page = 1
}

// Request context (for cancellation, timeouts)
ctx := c.Request.Context()

8. File Upload

func uploadFile(c *gin.Context) {
    file, err := c.FormFile("file")
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // Save file
    if err := c.SaveUploadedFile(file, "/uploads/"+file.Filename); err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(200, gin.H{"message": "File uploaded"})
}

MVP-Specific Patterns

Resource Flow Handler Example

type ResourceFlowHandler struct {
    service *ResourceFlowService
}

func (h *ResourceFlowHandler) Create(c *gin.Context) {
    var req CreateResourceFlowRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // Validate business belongs to user
    userID := c.MustGet("userID").(string)
    if !h.service.ValidateOwnership(req.BusinessID, userID) {
        c.JSON(403, gin.H{"error": "Forbidden"})
        return
    }
    
    flow, err := h.service.Create(c.Request.Context(), req)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(201, flow)
}

func (h *ResourceFlowHandler) FindMatches(c *gin.Context) {
    flowID := c.Param("id")
    matches, err := h.service.FindMatches(c.Request.Context(), flowID)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(200, gin.H{"matches": matches})
}

// Register routes
func setupRoutes(r *gin.Engine, handlers *Handlers) {
    api := r.Group("/api/v1")
    
    resources := api.Group("/resource-flows")
    resources.Use(authMiddleware())
    {
        resources.POST("", handlers.ResourceFlow.Create)
        resources.GET("/:id/matches", handlers.ResourceFlow.FindMatches)
    }
}

Middleware for Our MVP

// CORS middleware
func corsMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
        
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        
        c.Next()
    }
}

// Rate limiting (simple in-memory)
func rateLimitMiddleware() gin.HandlerFunc {
    // Use token bucket or sliding window
    return func(c *gin.Context) {
        // Check rate limit
        c.Next()
    }
}

Performance Tips

  1. Use gin.New() instead of gin.Default() if you don't need logger/recovery
  2. Bind only what you need - avoid binding large structs
  3. Use context for cancellation - c.Request.Context() for timeouts
  4. Reuse gin.Engine - don't create new engine per request
  5. Use c.Set() and c.Get() for request-scoped values

Tutorials & Resources


Common Patterns

Graceful Shutdown

func main() {
    r := gin.Default()
    // ... setup routes ...
    
    srv := &http.Server{
        Addr:    ":8080",
        Handler: r,
    }
    
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()
    
    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt)
    <-quit
    
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }
}

Structured Logging with Gin

import "github.com/rs/zerolog/log"

func loggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        
        c.Next()
        
        log.Info().
            Str("method", c.Request.Method).
            Str("path", path).
            Int("status", c.Writer.Status()).
            Dur("latency", time.Since(start)).
            Msg("HTTP request")
    }
}

Best Practices

  1. Don't use global variables - pass dependencies via handlers
  2. Use dependency injection - inject services into handlers
  3. Validate early - use binding validation
  4. Handle errors consistently - create error response helpers
  5. Use middleware for cross-cutting concerns - auth, logging, CORS
  6. Version your API - use route groups (/api/v1, /api/v2)
  7. Document your API - consider OpenAPI/Swagger integration