Taskw

Handler Functions

Understanding handler function structure and best practices

Handler Functions

Handler functions are the HTTP endpoints of your API. They receive requests, process them, and return responses. Taskw scans for these functions and generates route registration code for Fiber.

Overview

Handler functions are methods on handler structs that:

  • Accept a *fiber.Ctx parameter
  • Return an error
  • Have @Router annotations defining their HTTP routes

For route generation, Taskw specifically looks for:

  • Functions with "Provide" prefix and "Handler" suffix (e.g., ProvideUserHandler)
  • These functions should return handler structs that contain the actual handler methods

Basic Handler Structure

Simple Handler

type UserHandler struct {
    service *UserService
}

// @Router /users [get]
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    users, err := h.service.GetUsers()
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": err.Error(),
        })
    }
    return c.JSON(users)
}

// Provider function for dependency injection
func ProvideUserHandler(service *UserService) *UserHandler {
    return &UserHandler{service: service}
}

Handler with Dependencies

type UserHandler struct {
    service *UserService
    logger  *log.Logger
}

// @Router GET /users/{id}
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
    id := c.Params("id")
    
    h.logger.Printf("Getting user with ID: %s", id)
    
    user, err := h.service.GetUser(id)
    if err != nil {
        h.logger.Printf("Error getting user: %v", err)
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "User not found",
        })
    }
    
    return c.JSON(user)
}

Handler Requirements

Must Have Receiver

// ✅ Correct - has receiver
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    // Implementation
}

// ❌ Incorrect - no receiver
func GetUsers(c *fiber.Ctx) error {
    // Implementation
}

Must Accept Fiber Context

// ✅ Correct - accepts *fiber.Ctx
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    // Implementation
}

// ❌ Incorrect - wrong parameter type
func (h *UserHandler) GetUsers(c *gin.Context) error {
    // Implementation
}

Must Return Error

// ✅ Correct - returns error
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    // Implementation
}

// ❌ Incorrect - doesn't return error
func (h *UserHandler) GetUsers(c *fiber.Ctx) {
    // Implementation
}

Must Have @Router Annotation

// ✅ Correct - has @Router annotation
// @Router GET /users
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    // Implementation
}

// ❌ Incorrect - missing @Router annotation
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    // Implementation
}

Handler Patterns

RESTful CRUD Pattern

type UserHandler struct {
    service *UserService
}

// @Router GET /users
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    users, err := h.service.GetUsers()
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": err.Error(),
        })
    }
    return c.JSON(users)
}

// @Router GET /users/{id}
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
    id := c.Params("id")
    user, err := h.service.GetUser(id)
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "User not found",
        })
    }
    return c.JSON(user)
}

// @Router POST /users
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    var user User
    if err := c.BodyParser(&user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid request body",
        })
    }
    
    createdUser, err := h.service.CreateUser(&user)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": err.Error(),
        })
    }
    
    return c.Status(fiber.StatusCreated).JSON(createdUser)
}

// @Router PUT /users/{id}
func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
    id := c.Params("id")
    var user User
    if err := c.BodyParser(&user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid request body",
        })
    }
    
    updatedUser, err := h.service.UpdateUser(id, &user)
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "User not found",
        })
    }
    
    return c.JSON(updatedUser)
}

// @Router DELETE /users/{id}
func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
    id := c.Params("id")
    err := h.service.DeleteUser(id)
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "User not found",
        })
    }
    
    return c.SendStatus(fiber.StatusNoContent)
}

Health Check Pattern

type HealthHandler struct{}

// @Router GET /health
func (h *HealthHandler) GetHealth(c *fiber.Ctx) error {
    return c.JSON(fiber.Map{
        "status": "ok",
        "timestamp": time.Now().UTC(),
    })
}

// @Router GET /health/ready
func (h *HealthHandler) GetReadiness(c *fiber.Ctx) error {
    // Check if application is ready to serve traffic
    return c.JSON(fiber.Map{
        "status": "ready",
        "timestamp": time.Now().UTC(),
    })
}

// @Router GET /health/live
func (h *HealthHandler) GetLiveness(c *fiber.Ctx) error {
    // Check if application is alive
    return c.JSON(fiber.Map{
        "status": "alive",
        "timestamp": time.Now().UTC(),
    })
}

Search and Filter Pattern

type UserHandler struct {
    service *UserService
}

// @Router GET /users/search
func (h *UserHandler) SearchUsers(c *fiber.Ctx) error {
    query := c.Query("q")
    page := c.Query("page", "1")
    limit := c.Query("limit", "10")
    
    users, err := h.service.SearchUsers(query, page, limit)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": err.Error(),
        })
    }
    
    return c.JSON(users)
}

// @Router GET /users/by-email
func (h *UserHandler) GetUserByEmail(c *fiber.Ctx) error {
    email := c.Query("email")
    if email == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Email parameter is required",
        })
    }
    
    user, err := h.service.GetUserByEmail(email)
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "User not found",
        })
    }
    
    return c.JSON(user)
}

Request Handling

Path Parameters

// @Router GET /users/{id}
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
    id := c.Params("id")
    // Use id parameter
}

// @Router PUT /users/{id}/profile
func (h *UserHandler) UpdateUserProfile(c *fiber.Ctx) error {
    id := c.Params("id")
    // Use id parameter
}

// @Router GET /orders/{orderId}/items/{itemId}
func (h *OrderHandler) GetOrderItem(c *fiber.Ctx) error {
    orderId := c.Params("orderId")
    itemId := c.Params("itemId")
    // Use both parameters
}

Query Parameters

// @Router GET /users
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    page := c.Query("page", "1")
    limit := c.Query("limit", "10")
    search := c.Query("search")
    sortBy := c.Query("sort_by", "created_at")
    order := c.Query("order", "desc")
    
    // Use query parameters
}

Request Body

// @Router POST /users
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    var user User
    if err := c.BodyParser(&user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid request body",
        })
    }
    
    // Validate user data
    if user.Name == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Name is required",
        })
    }
    
    // Process user creation
}

Headers

// @Router GET /users/profile
func (h *UserHandler) GetProfile(c *fiber.Ctx) error {
    authToken := c.Get("Authorization")
    if authToken == "" {
        return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
            "error": "Authorization header required",
        })
    }
    
    // Validate token and get user profile
}

Response Handling

JSON Responses

// @Router GET /users
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    users, err := h.service.GetUsers()
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": err.Error(),
        })
    }
    
    return c.JSON(fiber.Map{
        "data": users,
        "count": len(users),
        "timestamp": time.Now().UTC(),
    })
}

Error Responses

// @Router GET /users/{id}
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
    id := c.Params("id")
    user, err := h.service.GetUser(id)
    
    if err != nil {
        if errors.Is(err, ErrUserNotFound) {
            return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
                "error": "User not found",
                "id": id,
            })
        }
        
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": "Internal server error",
        })
    }
    
    return c.JSON(user)
}

Status Codes

// @Router POST /users
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    // ... validation and creation logic
    
    return c.Status(fiber.StatusCreated).JSON(createdUser)
}

// @Router DELETE /users/{id}
func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
    // ... deletion logic
    
    return c.SendStatus(fiber.StatusNoContent)
}

// @Router PUT /users/{id}
func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
    // ... update logic
    
    return c.Status(fiber.StatusOK).JSON(updatedUser)
}

Error Handling

Centralized Error Handling

type UserHandler struct {
    service *UserService
    logger  *log.Logger
}

// @Router GET /users/{id}
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
    id := c.Params("id")
    
    user, err := h.service.GetUser(id)
    if err != nil {
        return h.handleError(c, err, "failed to get user")
    }
    
    return c.JSON(user)
}

func (h *UserHandler) handleError(c *fiber.Ctx, err error, message string) error {
    h.logger.Printf("%s: %v", message, err)
    
    if errors.Is(err, ErrUserNotFound) {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "User not found",
        })
    }
    
    if errors.Is(err, ErrValidation) {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": err.Error(),
        })
    }
    
    return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
        "error": "Internal server error",
    })
}

Validation Errors

// @Router POST /users
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    var user User
    if err := c.BodyParser(&user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid request body",
            "details": err.Error(),
        })
    }
    
    // Validate required fields
    if user.Name == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Name is required",
            "field": "name",
        })
    }
    
    if user.Email == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Email is required",
            "field": "email",
        })
    }
    
    // Validate email format
    if !isValidEmail(user.Email) {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid email format",
            "field": "email",
        })
    }
    
    // Process creation
}

Best Practices

Single Responsibility

// ✅ Single responsibility
// @Router GET /users
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    users, err := h.service.GetUsers()
    if err != nil {
        return h.handleError(c, err, "failed to get users")
    }
    return c.JSON(users)
}

// ❌ Multiple responsibilities
// @Router GET /users
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    // Authentication
    // Authorization
    // Validation
    // Business logic
    // Database queries
    // Response formatting
    // Logging
    // Error handling
}

Consistent Error Handling

// ✅ Consistent error handling
func (h *UserHandler) handleError(c *fiber.Ctx, err error, message string) error {
    h.logger.Printf("%s: %v", message, err)
    
    switch {
    case errors.Is(err, ErrNotFound):
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "Resource not found",
        })
    case errors.Is(err, ErrValidation):
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": err.Error(),
        })
    default:
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": "Internal server error",
        })
    }
}

Input Validation

// ✅ Proper input validation
// @Router POST /users
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    var user User
    if err := c.BodyParser(&user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid request body",
        })
    }
    
    if err := h.validateUser(&user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": err.Error(),
        })
    }
    
    createdUser, err := h.service.CreateUser(&user)
    if err != nil {
        return h.handleError(c, err, "failed to create user")
    }
    
    return c.Status(fiber.StatusCreated).JSON(createdUser)
}

func (h *UserHandler) validateUser(user *User) error {
    if user.Name == "" {
        return errors.New("name is required")
    }
    if user.Email == "" {
        return errors.New("email is required")
    }
    if !isValidEmail(user.Email) {
        return errors.New("invalid email format")
    }
    return nil
}

Logging

// ✅ Proper logging
type UserHandler struct {
    service *UserService
    logger  *log.Logger
}

// @Router GET /users/{id}
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
    id := c.Params("id")
    
    h.logger.Printf("Getting user with ID: %s", id)
    
    user, err := h.service.GetUser(id)
    if err != nil {
        h.logger.Printf("Error getting user %s: %v", id, err)
        return h.handleError(c, err, "failed to get user")
    }
    
    h.logger.Printf("Successfully retrieved user %s", id)
    return c.JSON(user)
}

Common Patterns

Pagination Pattern

// @Router GET /users
func (h *UserHandler) GetUsers(c *fiber.Ctx) error {
    page, _ := strconv.Atoi(c.Query("page", "1"))
    limit, _ := strconv.Atoi(c.Query("limit", "10"))
    
    if page < 1 {
        page = 1
    }
    if limit < 1 || limit > 100 {
        limit = 10
    }
    
    users, total, err := h.service.GetUsersPaginated(page, limit)
    if err != nil {
        return h.handleError(c, err, "failed to get users")
    }
    
    return c.JSON(fiber.Map{
        "data": users,
        "pagination": fiber.Map{
            "page": page,
            "limit": limit,
            "total": total,
            "pages": (total + limit - 1) / limit,
        },
    })
}

Search Pattern

// @Router GET /users/search
func (h *UserHandler) SearchUsers(c *fiber.Ctx) error {
    query := c.Query("q")
    if query == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Search query is required",
        })
    }
    
    users, err := h.service.SearchUsers(query)
    if err != nil {
        return h.handleError(c, err, "failed to search users")
    }
    
    return c.JSON(fiber.Map{
        "data": users,
        "query": query,
        "count": len(users),
    })
}

Bulk Operations Pattern

// @Router POST /users/bulk
func (h *UserHandler) CreateUsersBulk(c *fiber.Ctx) error {
    var users []User
    if err := c.BodyParser(&users); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid request body",
        })
    }
    
    createdUsers, errors := h.service.CreateUsersBulk(users)
    
    return c.JSON(fiber.Map{
        "created": createdUsers,
        "errors": errors,
        "summary": fiber.Map{
            "total": len(users),
            "successful": len(createdUsers),
            "failed": len(errors),
        },
    })
}

Troubleshooting

Common Issues

Handler not detected: Check receiver, parameter type, and return type

Route not generated: Ensure @Router annotation is present and correctly formatted

Parameter parsing errors: Validate request body and query parameters

Error handling: Use consistent error response format

Debugging Commands

# Scan to see what handlers are detected
taskw scan

# Generate route registration code
taskw generate routes

# Test the generated routes
go run ./cmd/server

Validation

Taskw validates handlers and reports issues:

❌ Validation errors:
  • UserHandler.GetUser: Missing @Router annotation
  • UserHandler.CreateUser: Invalid parameter type (expected *fiber.Ctx)
  • UserHandler.UpdateUser: Invalid return type (expected error)