Documentation

Routing

Overview

Routing maps HTTP requests to controller actions. The framework uses Gin router with organized route groups for different application sections.

Route Organization

Route Groups

// App routes for web interface
func (this *AppRouter) GardenRoutes() {
    gardens := this.Router.Group("/gardens")
    {
        gardens.GET("/", this.GardenController.List)
        gardens.GET("/new", this.GardenController.ShowNew)
        gardens.POST("/", this.GardenController.Create)
        gardens.GET("/:id", this.GardenController.Show)
        gardens.GET("/:id/edit", this.GardenController.ShowEdit)
        gardens.PUT("/:id", this.GardenController.Update)
        gardens.DELETE("/:id", this.GardenController.Delete)
    }
}

// API routes for JSON responses
func (this *ApiRouter) GardenRoutes() {
    api := this.Router.Group("/api/v1")
    gardens := api.Group("/gardens")
    {
        gardens.GET("/", this.GardenController.ApiList)
        gardens.POST("/", this.GardenController.ApiCreate)
        gardens.GET("/:id", this.GardenController.ApiShow)
        gardens.PUT("/:id", this.GardenController.ApiUpdate)
        gardens.DELETE("/:id", this.GardenController.ApiDelete)
    }
}

Basic Route Patterns

RESTful Routes

// Standard CRUD patterns
GET    /gardens      -> List all gardens
GET    /gardens/new  -> Show create form
POST   /gardens      -> Create garden
GET    /gardens/:id  -> Show garden
GET    /gardens/:id/edit -> Show edit form
PUT    /gardens/:id  -> Update garden
DELETE /gardens/:id  -> Delete garden

Route Parameters

func (this *GardenController) Show(c *gin.Context) {
    id := c.Param("id")  // Extract :id parameter
    
    garden, exists, err := this.GardenFetcher.FindOneById(c.Request.Context(), id, nil)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    if !exists {
        c.JSON(404, gin.H{"error": "Garden not found"})
        return
    }
    
    c.HTML(200, "gardens/show.html", gin.H{"garden": garden})
}

Query Parameters

func (this *GardenController) List(c *gin.Context) {
    // Extract query parameters
    search := c.Query("search")      // ?search=tomato
    page := c.DefaultQuery("page", "1") // ?page=2 (default 1)
    
    // Build fetcher modification
    mod := this.GardenFetcher.Mod()
    if search != "" {
        mod.ContainsStringValueFilter("name", search)
    }
    
    if pageInt, err := strconv.Atoi(page); err == nil {
        mod.Page = pageInt
    }
    
    gardens, pagination, err := this.GardenFetcher.FindPage(c.Request.Context(), mod)
    // ... render response
}

Route Registration

Router Setup

// src/app/routers/app_router.go
type AppRouter struct {
    Router           *gin.Engine
    GardenController *controllers.GardenController
    UserController   *controllers.UserController
}

func (this *AppRouter) RegisterRoutes() {
    // Register all route groups
    this.GardenRoutes()
    this.UserRoutes()
    this.PlantRoutes()
    
    // Static file serving
    this.Router.Static("/assets", "./assets")
    this.Router.StaticFile("/favicon.ico", "./assets/favicon.ico")
}

Middleware Application

func (this *AppRouter) RegisterRoutes() {
    // Global middleware
    this.Router.Use(middleware.Logger())
    this.Router.Use(middleware.Recovery())
    
    // Route group with authentication
    protected := this.Router.Group("/")
    protected.Use(middleware.Authentication())
    {
        protected.GET("/dashboard", this.DashboardController.Show)
        protected.POST("/gardens", this.GardenController.Create)
    }
    
    // Public routes
    this.Router.GET("/", this.HomeController.Index)
    this.Router.GET("/login", this.AuthController.ShowLogin)
}

Controller Integration

Basic Controller Pattern

type GardenController struct {
    GardenFetcher    *fetchers.GardenFetcher
    GardenSerializer *serializers.GardenSerializer
    CreateGardenMae  *mae.CreateGardenMae
    GardenBinder     *binders.GardenBinder
}

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": err.Error()})
        return
    }
    
    // 2. Execute business logic
    output, err := this.CreateGardenMae.Execute(c.Request.Context(), &maes.CreateGardenMaeInput{
        Form: form,
    })
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    // 3. Return response
    c.JSON(201, gin.H{"garden": output.Garden})
}

Response Types

HTML Responses

func (this *GardenController) Show(c *gin.Context) {
    // Load garden data
    garden := this.loadGarden(c)
    
    // Render HTML template
    c.HTML(200, "gardens/show.html", gin.H{
        "title":  "Garden Details",
        "garden": garden,
    })
}

JSON Responses

func (this *GardenController) ApiShow(c *gin.Context) {
    garden := this.loadGarden(c)
    
    // Serialize to JSON
    jsonData, err := this.GardenSerializer.OneToJson(garden)
    if err != nil {
        c.JSON(500, gin.H{"error": "Serialization failed"})
        return
    }
    
    c.Data(200, "application/json", jsonData)
}

Redirect Responses

func (this *GardenController) Update(c *gin.Context) {
    // Update logic...
    
    // Redirect after successful update
    c.Redirect(302, fmt.Sprintf("/gardens/%s", garden.Id))
}

Common Route Patterns

Nested Resources

// Gardens with plants
func (this *AppRouter) GardenPlantRoutes() {
    gardens := this.Router.Group("/gardens/:garden_id")
    {
        plants := gardens.Group("/plants")
        {
            plants.GET("/", this.PlantController.ListForGarden)
            plants.POST("/", this.PlantController.CreateForGarden)
            plants.GET("/:id", this.PlantController.Show)
        }
    }
}

Bulk Operations

// Bulk actions
func (this *ApiRouter) GardenBulkRoutes() {
    api := this.Router.Group("/api/v1/gardens")
    {
        api.POST("/bulk", this.GardenController.BulkCreate)
        api.PUT("/bulk", this.GardenController.BulkUpdate)
        api.DELETE("/bulk", this.GardenController.BulkDelete)
    }
}

Custom Actions

// Custom garden actions
func (this *AppRouter) GardenActionRoutes() {
    gardens := this.Router.Group("/gardens/:id")
    {
        gardens.POST("/water", this.GardenController.Water)
        gardens.POST("/harvest", this.GardenController.Harvest)
        gardens.GET("/report", this.GardenController.GenerateReport)
    }
}

Route Testing

Testing Route Handlers

func TestGardenController_Show(t *testing.T) {
    // Setup
    router := setupTestRouter()
    controller := setupGardenController()
    router.GET("/gardens/:id", controller.Show)
    
    // Create request
    req, _ := http.NewRequest("GET", "/gardens/123", nil)
    w := httptest.NewRecorder()
    
    // Execute
    router.ServeHTTP(w, req)
    
    // Assert
    assert.Equal(t, 200, w.Code)
    assert.Contains(t, w.Body.String(), "garden")
}

LLM Routing Notes

  • Routes are organized in dedicated router files by feature
  • Use route groups for logical organization and middleware application
  • Follow RESTful conventions for CRUD operations
  • Extract parameters with c.Param() and c.Query()
  • Return appropriate HTTP status codes
  • Test route handlers with HTTP test requests