Documentation

Controllers

Overview

Controllers handle HTTP requests, orchestrate business logic through Mae modules, and format responses. They serve as the bridge between the HTTP layer and business logic.

Controller Structure

Basic Controller Pattern

type GardenController struct {
    // Business Logic
    CreateGardenMae *mae.CreateGardenMae
    UpdateGardenMae *mae.UpdateGardenMae
    DeleteGardenMae *mae.DeleteGardenMae
    
    // Data Services
    GardenFetcher   *fetchers.GardenFetcher
    GardenHydrator  *hydrators.GardenHydrator
    GardenSerializer *serializers.GardenSerializer
    
    // Request Handling
    GardenBinder    *binders.GardenBinder
    GardenPresenter *presenters.GardenPresenter
    
    // Utilities
    AppUrler        *urlers.AppUrler
    Logger          *logger.Logger
}

CRUD Operations

Create Action

func (this *GardenController) Create(c *gin.Context) {
    // 1. Bind form data
    form := &forms.CreateGardenForm{}
    if err := this.GardenBinder.BindCreateForm(c, form); err != nil {
        c.JSON(400, gin.H{"error": "Invalid form data", "details": err.Error()})
        return
    }
    
    // 2. Execute business logic
    input := &maes.CreateGardenMaeInput{
        Form:   form,
        UserId: this.getCurrentUserId(c),
    }
    
    output, err := this.CreateGardenMae.Execute(c.Request.Context(), input)
    if err != nil {
        this.handleError(c, err)
        return
    }
    
    // 3. Format response
    location := this.AppUrler.ShowGarden(output.Garden.Id)
    c.Header("Location", location)
    c.JSON(201, gin.H{
        "garden": output.Garden,
        "success": true,
        "redirect": location,
    })
}

Show Action

func (this *GardenController) Show(c *gin.Context) {
    // 1. Extract parameters
    id := c.Param("id")
    if id == "" {
        c.JSON(400, gin.H{"error": "Garden ID is required"})
        return
    }
    
    // 2. Fetch entity
    garden, exists, err := this.GardenFetcher.FindOneById(c.Request.Context(), id, nil)
    if err != nil {
        c.JSON(500, gin.H{"error": "Failed to fetch garden"})
        return
    }
    
    if !exists {
        c.JSON(404, gin.H{"error": "Garden not found"})
        return
    }
    
    // 3. Hydrate relationships
    err = this.GardenHydrator.OneViaPreset(c.Request.Context(), garden, "show", nil)
    if err != nil {
        this.Logger.Error("Failed to hydrate garden relationships", "error", err)
        // Continue without relationships
    }
    
    // 4. Render response
    c.HTML(200, "gardens/show.html", gin.H{
        "title":  fmt.Sprintf("Garden: %s", garden.Name),
        "garden": garden,
    })
}

List Action

func (this *GardenController) List(c *gin.Context) {
    // 1. Build query parameters
    mod := this.GardenFetcher.Mod()
    
    // Search filter
    if search := c.Query("search"); search != "" {
        mod.ContainsStringValueFilter("name", search)
    }
    
    // Pagination
    if page, err := strconv.Atoi(c.DefaultQuery("page", "1")); err == nil && page > 0 {
        mod.Page = page
    }
    
    // 2. Fetch data
    gardens, pagination, err := this.GardenFetcher.FindPage(c.Request.Context(), mod)
    if err != nil {
        c.JSON(500, gin.H{"error": "Failed to fetch gardens"})
        return
    }
    
    // 3. Hydrate for list view
    err = this.GardenHydrator.ManyViaPreset(c.Request.Context(), gardens, "list", nil)
    if err != nil {
        this.Logger.Error("Failed to hydrate gardens", "error", err)
    }
    
    // 4. Render response
    c.HTML(200, "gardens/list.html", gin.H{
        "title":      "Gardens",
        "gardens":    gardens,
        "pagination": pagination,
        "search":     c.Query("search"),
    })
}

Update Action

func (this *GardenController) Update(c *gin.Context) {
    // 1. Extract ID and bind form
    id := c.Param("id")
    form := &forms.UpdateGardenForm{Id: id}
    
    if err := this.GardenBinder.BindUpdateForm(c, form); err != nil {
        c.JSON(400, gin.H{"error": "Invalid form data", "details": err.Error()})
        return
    }
    
    // 2. Execute update logic
    output, err := this.UpdateGardenMae.Execute(c.Request.Context(), &maes.UpdateGardenMaeInput{
        Form: form,
    })
    if err != nil {
        this.handleError(c, err)
        return
    }
    
    // 3. Handle response format
    if this.acceptsJSON(c) {
        c.JSON(200, gin.H{
            "garden": output.Garden,
            "success": true,
        })
    } else {
        c.Redirect(302, this.AppUrler.ShowGarden(output.Garden.Id))
    }
}

Delete Action

func (this *GardenController) Delete(c *gin.Context) {
    id := c.Param("id")
    
    // Execute delete logic
    err := this.DeleteGardenMae.Execute(c.Request.Context(), &maes.DeleteGardenMaeInput{
        Id: id,
    })
    if err != nil {
        this.handleError(c, err)
        return
    }
    
    if this.acceptsJSON(c) {
        c.JSON(204, nil) // No content
    } else {
        c.Redirect(302, this.AppUrler.ListGardens())
    }
}

Form Handling

Form Display Actions

func (this *GardenController) ShowNew(c *gin.Context) {
    // Show create form
    form := &forms.CreateGardenForm{}
    
    c.HTML(200, "gardens/new.html", gin.H{
        "title": "New Garden",
        "form":  form,
    })
}

func (this *GardenController) ShowEdit(c *gin.Context) {
    id := c.Param("id")
    
    // Load existing garden
    garden, exists, err := this.GardenFetcher.FindOneById(c.Request.Context(), id, nil)
    if err != nil || !exists {
        c.Redirect(302, this.AppUrler.ListGardens())
        return
    }
    
    // Populate form from garden
    form := &forms.UpdateGardenForm{}
    form = this.GardenMolder.ToForm(form, garden)
    
    c.HTML(200, "gardens/edit.html", gin.H{
        "title":  "Edit Garden",
        "garden": garden,
        "form":   form,
    })
}

API vs Web Responses

Dual Response Methods

// Web interface (HTML)
func (this *GardenController) List(c *gin.Context) {
    gardens := this.loadGardens(c)
    c.HTML(200, "gardens/list.html", gin.H{"gardens": gardens})
}

// API interface (JSON)
func (this *GardenController) ApiList(c *gin.Context) {
    gardens := this.loadGardens(c)
    
    jsonData, err := this.GardenSerializer.SetToJson(gardens)
    if err != nil {
        c.JSON(500, gin.H{"error": "Serialization failed"})
        return
    }
    
    c.Data(200, "application/json", jsonData)
}

Content Negotiation

func (this *GardenController) Show(c *gin.Context) {
    garden := this.loadGarden(c)
    
    if this.acceptsJSON(c) {
        jsonData, _ := this.GardenSerializer.OneToJson(garden)
        c.Data(200, "application/json", jsonData)
    } else {
        c.HTML(200, "gardens/show.html", gin.H{"garden": garden})
    }
}

func (this *GardenController) acceptsJSON(c *gin.Context) bool {
    return strings.Contains(c.GetHeader("Accept"), "application/json")
}

Error Handling

Structured Error Handling

func (this *GardenController) handleError(c *gin.Context, err error) {
    switch e := err.(type) {
    case *ValidationError:
        c.JSON(400, gin.H{
            "error": "Validation failed",
            "field": e.Field,
            "message": e.Message,
        })
    case *NotFoundError:
        c.JSON(404, gin.H{
            "error": "Garden not found",
        })
    case *BusinessRuleError:
        c.JSON(422, gin.H{
            "error": e.Message,
            "code":  e.Code,
        })
    default:
        this.Logger.Error("Unexpected error", "error", err)
        c.JSON(500, gin.H{
            "error": "Internal server error",
        })
    }
}

Helper Methods

Common Patterns

func (this *GardenController) getCurrentUserId(c *gin.Context) string {
    user, exists := c.Get("user")
    if !exists {
        return ""
    }
    return user.(*mdl.User).Id
}

func (this *GardenController) requireAuthentication(c *gin.Context) *mdl.User {
    user, exists := c.Get("user")
    if !exists {
        c.JSON(401, gin.H{"error": "Authentication required"})
        c.Abort()
        return nil
    }
    return user.(*mdl.User)
}

func (this *GardenController) requireOwnership(c *gin.Context, garden *mdl.Garden) bool {
    user := this.requireAuthentication(c)
    if user == nil {
        return false
    }
    
    if garden.UserId != user.Id {
        c.JSON(403, gin.H{"error": "Access denied"})
        c.Abort()
        return false
    }
    
    return true
}

Testing Controllers

HTTP Testing

func TestGardenController_Create(t *testing.T) {
    // Setup
    controller := setupTestController()
    router := gin.New()
    router.POST("/gardens", controller.Create)
    
    // Create request
    body := `{"name": "Test Garden", "description": "A test"}`
    req, _ := http.NewRequest("POST", "/gardens", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    
    w := httptest.NewRecorder()
    router.ServeHTTP(w, req)
    
    // Assert
    assert.Equal(t, 201, w.Code)
    
    var response map[string]interface{}
    json.Unmarshal(w.Body.Bytes(), &response)
    assert.True(t, response["success"].(bool))
}

LLM Controller Development Notes

  • Controllers orchestrate but don’t contain business logic
  • Use Mae modules for complex business operations
  • Handle both HTML and JSON responses appropriately
  • Implement proper error handling with structured responses
  • Extract common patterns into helper methods
  • Test controllers with HTTP requests
  • Follow RESTful conventions for action naming