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