Router

Basic Routing

Learn the fundamentals of HTTP routing with Forge

Forge's router provides a simple yet powerful way to handle HTTP requests. This guide covers the fundamentals of routing, including HTTP methods, path parameters, and basic route configuration.

HTTP Methods

Forge supports all standard HTTP methods with a consistent API:

// GET - Retrieve data
app.Router().GET("/users", getUsersHandler)

// POST - Create new resources
app.Router().POST("/users", createUserHandler)

// PUT - Update entire resources
app.Router().PUT("/users/:id", updateUserHandler)

// PATCH - Partial updates
app.Router().PATCH("/users/:id", patchUserHandler)

// DELETE - Remove resources
app.Router().DELETE("/users/:id", deleteUserHandler)

// OPTIONS - CORS preflight
app.Router().OPTIONS("/users", optionsHandler)

// HEAD - Get headers only
app.Router().HEAD("/users", headHandler)

Handler Functions

Basic Handler

func homeHandler(ctx forge.Context) error {
    return ctx.JSON(200, map[string]string{
        "message": "Welcome to Forge!",
    })
}

app.Router().GET("/", homeHandler)

Inline Handlers

app.Router().GET("/", func(ctx forge.Context) error {
    return ctx.JSON(200, map[string]string{
        "message": "Hello, World!",
    })
})

Handler with Error Handling

func getUserHandler(ctx forge.Context) error {
    id := ctx.Param("id")
    
    user, err := userService.GetUser(id)
    if err != nil {
        if errors.Is(err, ErrUserNotFound) {
            return forge.NotFound("user not found")
        }
        return forge.InternalError("failed to get user")
    }
    
    return ctx.JSON(200, user)
}

Path Parameters

Single Parameter

// Route: /users/:id
app.Router().GET("/users/:id", func(ctx forge.Context) error {
    id := ctx.Param("id")
    return ctx.JSON(200, map[string]string{"id": id})
})

Multiple Parameters

// Route: /users/:userId/posts/:postId
app.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{
        "userId": userID,
        "postId": postID,
    })
})

Wildcard Routes

// Route: /files/*
app.Router().GET("/files/*", func(ctx forge.Context) error {
    path := ctx.Param("*") // Gets everything after /files/
    return ctx.JSON(200, map[string]string{"path": path})
})

Request Handling

JSON Request Body

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

func createUserHandler(ctx forge.Context) error {
    var req CreateUserRequest
    
    // Bind JSON request body
    if err := ctx.BindJSON(&req); err != nil {
        return forge.BadRequest("invalid JSON")
    }
    
    // Validate request
    if err := validate.Struct(req); err != nil {
        return forge.BadRequest("validation failed")
    }
    
    // Process request
    user, err := userService.CreateUser(req)
    if err != nil {
        return forge.InternalError("failed to create user")
    }
    
    return ctx.JSON(201, user)
}

Form Data

func updateUserHandler(ctx forge.Context) error {
    var req UpdateUserRequest
    
    // Bind form data
    if err := ctx.BindForm(&req); err != nil {
        return forge.BadRequest("invalid form data")
    }
    
    // Process update
    user, err := userService.UpdateUser(req)
    if err != nil {
        return forge.InternalError("failed to update user")
    }
    
    return ctx.JSON(200, user)
}

Query Parameters

func getUsersHandler(ctx forge.Context) error {
    // Get query parameters
    page := ctx.Query("page", "1")
    limit := ctx.Query("limit", "10")
    search := ctx.Query("search", "")
    
    // Convert to integers
    pageNum, _ := strconv.Atoi(page)
    limitNum, _ := strconv.Atoi(limit)
    
    // Build filter
    filter := UserFilter{
        Page:   pageNum,
        Limit:  limitNum,
        Search: search,
    }
    
    users, err := userService.GetUsers(filter)
    if err != nil {
        return forge.InternalError("failed to get users")
    }
    
    return ctx.JSON(200, users)
}

Response Handling

JSON Response

func getUsersHandler(ctx forge.Context) error {
    users := []User{
        {ID: 1, Name: "John Doe", Email: "john@example.com"},
        {ID: 2, Name: "Jane Smith", Email: "jane@example.com"},
    }
    
    return ctx.JSON(200, users)
}

Custom Response

func downloadFileHandler(ctx forge.Context) error {
    filename := ctx.Param("filename")
    
    // Set custom headers
    ctx.Header("Content-Type", "application/octet-stream")
    ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
    
    // Stream file content
    file, err := os.Open(fmt.Sprintf("files/%s", filename))
    if err != nil {
        return forge.NotFound("file not found")
    }
    defer file.Close()
    
    return ctx.Stream(200, "application/octet-stream", file)
}

Error Responses

func errorHandler(ctx forge.Context) error {
    // Different error types
    return forge.BadRequest("invalid request")
    return forge.Unauthorized("authentication required")
    return forge.Forbidden("access denied")
    return forge.NotFound("resource not found")
    return forge.InternalError("server error")
    
    // Custom error with status code
    return forge.NewHTTPError(422, "validation failed", map[string]string{
        "field": "email",
        "error": "invalid format",
    })
}

Route Configuration

Basic Route Options

app.Router().GET("/users", handler,
    forge.WithName("getUsers"),
    forge.WithSummary("Get all users"),
    forge.WithDescription("Retrieves a list of all users with pagination"),
    forge.WithTags("users", "api"),
)

Route with Timeout

app.Router().POST("/users", handler,
    forge.WithTimeout(30*time.Second),
    forge.WithName("createUser"),
)

Route with Metadata

app.Router().GET("/users", handler,
    forge.WithMetadata("version", "v1"),
    forge.WithMetadata("deprecated", false),
    forge.WithMetadata("rate_limit", "100/hour"),
)

Route Groups

Basic Group

// API v1 group
apiV1 := app.Router().Group("/api/v1")

apiV1.GET("/users", getUsersHandler)
apiV1.POST("/users", createUserHandler)
apiV1.GET("/users/:id", getUserHandler)
apiV1.PUT("/users/:id", updateUserHandler)
apiV1.DELETE("/users/:id", deleteUserHandler)

Group with Middleware

// Admin group with authentication
admin := app.Router().Group("/admin", forge.WithGroupMiddleware(authMiddleware))

admin.GET("/dashboard", dashboardHandler)
admin.POST("/settings", updateSettingsHandler)
admin.GET("/users", adminGetUsersHandler)

Nested Groups

// API v1 group
apiV1 := app.Router().Group("/api/v1")

// Users sub-group
users := apiV1.Group("/users")
users.GET("/", getUsersHandler)
users.POST("/", createUserHandler)

// Posts sub-group
posts := apiV1.Group("/posts")
posts.GET("/", getPostsHandler)
posts.POST("/", createPostHandler)

Route Inspection

List All Routes

func listRoutesHandler(ctx forge.Context) error {
    routes := app.Router().Routes()
    
    var routeList []map[string]interface{}
    for _, route := range routes {
        routeList = append(routeList, map[string]interface{}{
            "name":    route.Name,
            "method":  route.Method,
            "path":    route.Path,
            "pattern": route.Pattern,
            "tags":    route.Tags,
        })
    }
    
    return ctx.JSON(200, routeList)
}

Find Route by Name

func getRouteInfoHandler(ctx forge.Context) error {
    routeName := ctx.Param("name")
    
    route, found := app.Router().RouteByName(routeName)
    if !found {
        return forge.NotFound("route not found")
    }
    
    return ctx.JSON(200, route)
}

Find Routes by Tag

func getRoutesByTagHandler(ctx forge.Context) error {
    tag := ctx.Param("tag")
    
    routes := app.Router().RoutesByTag(tag)
    
    return ctx.JSON(200, routes)
}

Best Practices

  1. Use Descriptive Route Names: Choose clear, descriptive names for routes
  2. Consistent URL Structure: Follow RESTful conventions
  3. Proper Error Handling: Return appropriate HTTP status codes
  4. Input Validation: Validate all input data
  5. Use Route Groups: Organize related routes together
  6. Document Routes: Use OpenAPI options for documentation
  7. Handle Edge Cases: Consider all possible scenarios

Common Patterns

RESTful API Structure

// Users resource
users := app.Router().Group("/users")
users.GET("/", getUsersHandler)           // GET /users
users.POST("/", createUserHandler)       // POST /users
users.GET("/:id", getUserHandler)        // GET /users/:id
users.PUT("/:id", updateUserHandler)     // PUT /users/:id
users.DELETE("/:id", deleteUserHandler)  // DELETE /users/:id

// Posts resource
posts := app.Router().Group("/posts")
posts.GET("/", getPostsHandler)          // GET /posts
posts.POST("/", createPostHandler)       // POST /posts
posts.GET("/:id", getPostHandler)        // GET /posts/:id
posts.PUT("/:id", updatePostHandler)     // PUT /posts/:id
posts.DELETE("/:id", deletePostHandler)  // DELETE /posts/:id

Health Check Endpoint

app.Router().GET("/health", func(ctx forge.Context) error {
    return ctx.JSON(200, map[string]string{
        "status": "healthy",
        "timestamp": time.Now().Format(time.RFC3339),
    })
})

API Versioning

// Version 1
v1 := app.Router().Group("/api/v1")
v1.GET("/users", getUsersV1Handler)

// Version 2
v2 := app.Router().Group("/api/v2")
v2.GET("/users", getUsersV2Handler)

For more advanced routing features, see the Middleware and Route Groups documentation.

How is this guide?

Last updated on