Router

Route Parameters

Learn how to use route parameters, path patterns, and route matching in Forge router for dynamic URL handling.

Route Parameters

Forge router provides powerful support for route parameters, allowing you to capture dynamic segments from URLs and use them in your handlers. This enables you to build flexible APIs with dynamic routing patterns.

Basic Route Parameters

Colon Syntax

The most common way to define route parameters is using the colon (:) syntax:

package main

import (
    "github.com/xraph/forge"
)

func main() {
    app := forge.New()
    router := app.Router()

    // Single parameter
    router.GET("/users/:id", func(ctx *forge.Context) error {
        userID := ctx.Param("id")
        return ctx.JSON(200, map[string]string{
            "user_id": userID,
        })
    })

    // Multiple parameters
    router.GET("/users/:userID/posts/:postID", func(ctx *forge.Context) error {
        userID := ctx.Param("userID")
        postID := ctx.Param("postID")
        
        return ctx.JSON(200, map[string]string{
            "user_id": userID,
            "post_id": postID,
        })
    })

    app.Start(":8080")
}

Brace Syntax

Forge also supports OpenAPI-style brace ({}) syntax for parameters:

// OpenAPI-style parameters
router.GET("/api/v1/users/{id}", func(ctx *forge.Context) error {
    userID := ctx.Param("id")
    return ctx.JSON(200, map[string]string{
        "user_id": userID,
    })
})

// Mixed syntax (both work)
router.GET("/organizations/{orgID}/teams/:teamID", func(ctx *forge.Context) error {
    orgID := ctx.Param("orgID")
    teamID := ctx.Param("teamID")
    
    return ctx.JSON(200, map[string]string{
        "organization_id": orgID,
        "team_id":         teamID,
    })
})

Parameter Extraction

Accessing Parameters

Use the Param() method to extract route parameters:

router.GET("/products/:category/:id", func(ctx *forge.Context) error {
    category := ctx.Param("category")
    productID := ctx.Param("id")
    
    // Validate parameters
    if category == "" || productID == "" {
        return ctx.JSON(400, map[string]string{
            "error": "Missing required parameters",
        })
    }
    
    return ctx.JSON(200, map[string]interface{}{
        "category":   category,
        "product_id": productID,
        "path":       ctx.Path(),
    })
})

Getting All Parameters

Retrieve all route parameters at once:

router.GET("/api/:version/users/:userID/settings/:setting", func(ctx *forge.Context) error {
    params := ctx.Params()
    
    return ctx.JSON(200, map[string]interface{}{
        "all_params": params,
        "version":    params["version"],
        "user_id":    params["userID"],
        "setting":    params["setting"],
    })
})

Wildcard Routes

Basic Wildcards

Use * for wildcard matching to capture remaining path segments:

// Catch-all wildcard
router.GET("/static/*", func(ctx *forge.Context) error {
    filepath := ctx.Param("*")
    
    return ctx.JSON(200, map[string]string{
        "requested_file": filepath,
        "full_path":      "/static/" + filepath,
    })
})

// Named wildcard
router.GET("/files/*filepath", func(ctx *forge.Context) error {
    filepath := ctx.Param("filepath")
    
    // Handle file serving logic
    return ctx.JSON(200, map[string]string{
        "file_path": filepath,
    })
})

Advanced Wildcard Patterns

// Wildcard with prefix matching
router.GET("/dashboard/*", func(ctx *forge.Context) error {
    remainingPath := ctx.Param("*")
    
    // Route to different dashboard sections
    switch {
    case strings.HasPrefix(remainingPath, "analytics"):
        return handleAnalytics(ctx, remainingPath)
    case strings.HasPrefix(remainingPath, "settings"):
        return handleSettings(ctx, remainingPath)
    default:
        return handleDashboardHome(ctx)
    }
})

// Multiple wildcards in groups
adminGroup := router.Group("/admin")
adminGroup.GET("/users/*action", handleUserActions)
adminGroup.GET("/system/*config", handleSystemConfig)

Parameter Validation

Type Conversion and Validation

import (
    "strconv"
    "regexp"
)

// Numeric parameter validation
router.GET("/users/:id", func(ctx *forge.Context) error {
    idStr := ctx.Param("id")
    
    // Convert to integer
    userID, err := strconv.Atoi(idStr)
    if err != nil {
        return ctx.JSON(400, map[string]string{
            "error": "Invalid user ID format",
        })
    }
    
    if userID <= 0 {
        return ctx.JSON(400, map[string]string{
            "error": "User ID must be positive",
        })
    }
    
    return ctx.JSON(200, map[string]interface{}{
        "user_id": userID,
    })
})

// UUID parameter validation
var uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)

router.GET("/resources/:uuid", func(ctx *forge.Context) error {
    uuid := ctx.Param("uuid")
    
    if !uuidRegex.MatchString(uuid) {
        return ctx.JSON(400, map[string]string{
            "error": "Invalid UUID format",
        })
    }
    
    return ctx.JSON(200, map[string]string{
        "resource_uuid": uuid,
    })
})

Custom Parameter Validators

// Create reusable parameter validators
func ValidateUserID(ctx *forge.Context) error {
    idStr := ctx.Param("id")
    if idStr == "" {
        return ctx.JSON(400, map[string]string{"error": "User ID required"})
    }
    
    id, err := strconv.Atoi(idStr)
    if err != nil || id <= 0 {
        return ctx.JSON(400, map[string]string{"error": "Invalid user ID"})
    }
    
    // Store validated ID in context
    ctx.Set("validated_user_id", id)
    return nil
}

// Use validator as middleware
router.GET("/users/:id", ValidateUserID, func(ctx *forge.Context) error {
    userID := ctx.Get("validated_user_id").(int)
    
    return ctx.JSON(200, map[string]interface{}{
        "user_id": userID,
    })
})

Route Patterns

Pattern Matching

Forge supports various route pattern formats:

// Exact match
router.GET("/api/health", healthCheck)

// Parameter segments
router.GET("/api/users/:id", getUser)

// Multiple parameters
router.GET("/api/users/:userID/posts/:postID/comments/:commentID", getComment)

// Optional segments (using groups)
apiGroup := router.Group("/api/v1")
apiGroup.GET("/users", listUsers)           // /api/v1/users
apiGroup.GET("/users/:id", getUser)         // /api/v1/users/123

// Wildcard patterns
router.GET("/static/*filepath", serveStatic)
router.GET("/docs/*", serveDocs)

Route Priority

Routes are matched in the order they are registered. More specific routes should be registered before more general ones:

// Register specific routes first
router.GET("/users/profile", getCurrentUserProfile)
router.GET("/users/settings", getUserSettings)
router.GET("/users/new", showNewUserForm)

// Then register parameterized routes
router.GET("/users/:id", getUser)

// Finally, wildcard routes
router.GET("/users/*", handleUserWildcard)

Advanced Parameter Features

Parameter Constraints

// Custom parameter constraints using middleware
func WithIDConstraint(min, max int) forge.Middleware {
    return func(next forge.HandlerFunc) forge.HandlerFunc {
        return func(ctx *forge.Context) error {
            idStr := ctx.Param("id")
            id, err := strconv.Atoi(idStr)
            if err != nil {
                return ctx.JSON(400, map[string]string{
                    "error": "Invalid ID format",
                })
            }
            
            if id < min || id > max {
                return ctx.JSON(400, map[string]string{
                    "error": fmt.Sprintf("ID must be between %d and %d", min, max),
                })
            }
            
            return next(ctx)
        }
    }
}

// Apply constraint to routes
router.GET("/users/:id", WithIDConstraint(1, 999999), getUser)

Parameter Transformation

// Transform parameters before handler execution
func TransformSlug(ctx *forge.Context) error {
    slug := ctx.Param("slug")
    
    // Transform slug to ID (example: lookup in database)
    id, err := lookupIDBySlug(slug)
    if err != nil {
        return ctx.JSON(404, map[string]string{
            "error": "Resource not found",
        })
    }
    
    // Store transformed value
    ctx.Set("resource_id", id)
    return nil
}

router.GET("/articles/:slug", TransformSlug, func(ctx *forge.Context) error {
    resourceID := ctx.Get("resource_id").(int)
    
    return ctx.JSON(200, map[string]interface{}{
        "resource_id": resourceID,
    })
})

Parameter Documentation

For OpenAPI documentation, specify parameter schemas:

import "github.com/xraph/forge"

// Document parameters for OpenAPI
router.GET("/users/:id", getUser,
    forge.WithName("getUser"),
    forge.WithSummary("Get user by ID"),
    forge.WithDescription("Retrieves a user by their unique identifier"),
    forge.WithTags("users"),
    forge.WithParameter("id", "path", "User ID", true, "123"),
)

// Document query parameters
router.GET("/users", listUsers,
    forge.WithName("listUsers"),
    forge.WithQuerySchema(&ListUsersQuery{}),
)

type ListUsersQuery struct {
    Page     int    `query:"page" validate:"min=1" example:"1"`
    PageSize int    `query:"page_size" validate:"min=1,max=100" example:"20"`
    Search   string `query:"search" example:"john"`
}

Error Handling

Parameter Error Handling

// Centralized parameter error handling
func HandleParameterErrors(ctx *forge.Context) error {
    // Check for required parameters
    requiredParams := []string{"id", "category"}
    
    for _, param := range requiredParams {
        if ctx.Param(param) == "" {
            return ctx.JSON(400, map[string]interface{}{
                "error": "Missing required parameter",
                "parameter": param,
                "path": ctx.Path(),
            })
        }
    }
    
    return nil
}

// Use in route groups
apiGroup := router.Group("/api/v1")
apiGroup.Use(HandleParameterErrors)
apiGroup.GET("/products/:category/:id", getProduct)

Custom Error Responses

// Custom parameter validation errors
type ParameterError struct {
    Parameter string `json:"parameter"`
    Value     string `json:"value"`
    Message   string `json:"message"`
}

func ValidateProductID(ctx *forge.Context) error {
    idStr := ctx.Param("id")
    
    if idStr == "" {
        return ctx.JSON(400, ParameterError{
            Parameter: "id",
            Value:     "",
            Message:   "Product ID is required",
        })
    }
    
    if len(idStr) < 3 {
        return ctx.JSON(400, ParameterError{
            Parameter: "id",
            Value:     idStr,
            Message:   "Product ID must be at least 3 characters",
        })
    }
    
    return nil
}

Best Practices

Parameter Best Practices

  1. Consistent Naming: Use consistent parameter naming conventions (camelCase or snake_case)
  2. Validation: Always validate parameters before using them
  3. Documentation: Document parameters for API consumers
  4. Error Handling: Provide clear error messages for invalid parameters
  5. Type Safety: Convert and validate parameter types early
// 1. Use descriptive parameter names
router.GET("/users/:userID/orders/:orderID", getOrder)  // Good
router.GET("/users/:id1/orders/:id2", getOrder)         // Avoid

// 2. Group related parameters
router.GET("/api/:version/users/:userID", getUser)

// 3. Use middleware for common validations
router.Use(ValidateAPIVersion)
router.GET("/api/:version/users/:userID", getUser)

// 4. Provide parameter examples in documentation
router.GET("/products/:category/:id", getProduct,
    forge.WithParameter("category", "path", "Product category", true, "electronics"),
    forge.WithParameter("id", "path", "Product ID", true, "12345"),
)

Next Steps

How is this guide?

Last updated on