Examples

This section provides practical examples of using HX in various scenarios.

REST API Example

Here’s a complete example of a REST API for managing users:

package main

import (
    "context"
    "fmt"
    "net/http"
    "strconv"
    "sync"

    "github.com/eatmoreapple/hx"
    . "github.com/eatmoreapple/hx/httpx"
)

// User model
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// In-memory storage
var (
    users   = make(map[int]User)
    usersMu = sync.RWMutex{}
    nextID  = 1
)

// Extractors
type UserID string
func (u UserID) ValueName() string { return "id" }

// Request types
type GetUserRequest struct {
    ID FromPath[UserID] `json:"id"`
}

type CreateUserRequest struct {
    Name  string `json:"name" form:"name"`
    Email string `json:"email" form:"email"`
}

type UpdateUserRequest struct {
    ID    FromPath[UserID] `json:"id"`
    Name  string           `json:"name" form:"name"`
    Email string           `json:"email" form:"email"`
}

type DeleteUserRequest struct {
    ID FromPath[UserID] `json:"id"`
}

// Handlers
func listUsers(ctx context.Context, req Empty) ([]User, error) {
    usersMu.RLock()
    defer usersMu.RUnlock()

    userList := make([]User, 0, len(users))
    for _, user := range users {
        userList = append(userList, user)
    }
    return userList, nil
}

func getUser(ctx context.Context, req GetUserRequest) (User, error) {
    id, err := strconv.Atoi(string(req.ID))
    if err != nil {
        return User{}, fmt.Errorf("invalid user ID: %v", err)
    }

    usersMu.RLock()
    defer usersMu.RUnlock()

    user, exists := users[id]
    if !exists {
        return User{}, fmt.Errorf("user not found")
    }
    return user, nil
}

func createUser(ctx context.Context, req CreateUserRequest) (User, error) {
    if req.Name == "" || req.Email == "" {
        return User{}, fmt.Errorf("name and email are required")
    }

    usersMu.Lock()
    defer usersMu.Unlock()

    user := User{
        ID:    nextID,
        Name:  req.Name,
        Email: req.Email,
    }
    users[nextID] = user
    nextID++

    return user, nil
}

func updateUser(ctx context.Context, req UpdateUserRequest) (User, error) {
    id, err := strconv.Atoi(string(req.ID))
    if err != nil {
        return User{}, fmt.Errorf("invalid user ID: %v", err)
    }

    usersMu.Lock()
    defer usersMu.Unlock()

    user, exists := users[id]
    if !exists {
        return User{}, fmt.Errorf("user not found")
    }

    if req.Name != "" {
        user.Name = req.Name
    }
    if req.Email != "" {
        user.Email = req.Email
    }

    users[id] = user
    return user, nil
}

func deleteUser(ctx context.Context, req DeleteUserRequest) (map[string]string, error) {
    id, err := strconv.Atoi(string(req.ID))
    if err != nil {
        return nil, fmt.Errorf("invalid user ID: %v", err)
    }

    usersMu.Lock()
    defer usersMu.Unlock()

    if _, exists := users[id]; !exists {
        return nil, fmt.Errorf("user not found")
    }

    delete(users, id)
    return map[string]string{"message": "user deleted successfully"}, nil
}

func main() {
    router := hx.New()

    // Routes
    router.GET("/users", hx.E(listUsers).JSON())
    router.GET("/users/{id}", hx.G(getUser).JSON())
    router.POST("/users", hx.G(createUser).JSON())
    router.PUT("/users/{id}", hx.G(updateUser).JSON())
    router.DELETE("/users/{id}", hx.G(deleteUser).JSON())

    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", router)
}

Test the API:

# List users
curl http://localhost:8080/users

# Create user
curl -X POST http://localhost:8080/users \
     -H "Content-Type: application/json" \
     -d '{"name":"John Doe","email":"john@example.com"}'

# Get user
curl http://localhost:8080/users/1

# Update user
curl -X PUT http://localhost:8080/users/1 \
     -H "Content-Type: application/json" \
     -d '{"name":"Jane Doe"}'

# Delete user
curl -X DELETE http://localhost:8080/users/1

File Upload Example

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "path/filepath"

    "github.com/eatmoreapple/hx"
    . "github.com/eatmoreapple/hx/httpx"
)

type FileUploadRequest struct {
    Description string `form:"description"`
    // File will be extracted from multipart form
}

type FileUploadResponse struct {
    Filename    string `json:"filename"`
    Size        int64  `json:"size"`
    Description string `json:"description"`
    Message     string `json:"message"`
}

func uploadFile(ctx context.Context, req FileUploadRequest) (FileUploadResponse, error) {
    // Get the HTTP request from context (you'll need to pass it)
    r := ctx.Value("http_request").(*http.Request)

    file, header, err := r.FormFile("file")
    if err != nil {
        return FileUploadResponse{}, fmt.Errorf("failed to get file: %v", err)
    }
    defer file.Close()

    // Create uploads directory
    uploadDir := "./uploads"
    os.MkdirAll(uploadDir, 0755)

    // Create destination file
    dst, err := os.Create(filepath.Join(uploadDir, header.Filename))
    if err != nil {
        return FileUploadResponse{}, fmt.Errorf("failed to create file: %v", err)
    }
    defer dst.Close()

    // Copy file content
    size, err := io.Copy(dst, file)
    if err != nil {
        return FileUploadResponse{}, fmt.Errorf("failed to save file: %v", err)
    }

    return FileUploadResponse{
        Filename:    header.Filename,
        Size:        size,
        Description: req.Description,
        Message:     "File uploaded successfully",
    }, nil
}

func main() {
    router := hx.New()

    // Serve upload form
    router.GET("/upload", hx.Warp(func(w http.ResponseWriter, r *http.Request) {
        html := `
        <html>
        <body>
            <form action="/upload" method="post" enctype="multipart/form-data">
                <input type="file" name="file" required><br><br>
                <input type="text" name="description" placeholder="Description"><br><br>
                <input type="submit" value="Upload">
            </form>
        </body>
        </html>`
        w.Header().Set("Content-Type", "text/html")
        w.Write([]byte(html))
    }))

    router.POST("/upload", hx.G(uploadFile).JSON())

    fmt.Println("Server starting on :8080")
    fmt.Println("Visit http://localhost:8080/upload to upload files")
    http.ListenAndServe(":8080", router)
}

Middleware Example

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/eatmoreapple/hx"
    . "github.com/eatmoreapple/hx/httpx"
)

// Logging middleware
func loggingMiddleware(next hx.HandlerFunc) hx.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) error {
        start := time.Now()
        log.Printf("Started %s %s", r.Method, r.URL.Path)

        err := next(w, r)

        duration := time.Since(start)
        log.Printf("Completed %s %s in %v", r.Method, r.URL.Path, duration)

        return err
    }
}

// CORS middleware
func corsMiddleware(next hx.HandlerFunc) hx.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) error {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return nil
        }

        return next(w, r)
    }
}

// Authentication middleware
func authMiddleware(next hx.HandlerFunc) hx.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) error {
        token := r.Header.Get("Authorization")
        if token == "" {
            return fmt.Errorf("authorization header required")
        }

        // Validate token (simplified)
        if token != "Bearer valid-token" {
            return fmt.Errorf("invalid token")
        }

        return next(w, r)
    }
}

func publicHandler(ctx context.Context, req Empty) (string, error) {
    return "This is a public endpoint", nil
}

func protectedHandler(ctx context.Context, req Empty) (string, error) {
    return "This is a protected endpoint", nil
}

func main() {
    router := hx.New()

    // Global middleware
    router.Use(loggingMiddleware, corsMiddleware)

    // Public routes
    router.GET("/public", hx.E(publicHandler).String())

    // Protected routes group
    protected := router.Group("/api")
    protected.Use(authMiddleware)
    protected.GET("/protected", hx.E(protectedHandler).String())

    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", router)
}

Test the middleware:

# Public endpoint (works)
curl http://localhost:8080/public

# Protected endpoint without auth (fails)
curl http://localhost:8080/api/protected

# Protected endpoint with auth (works)
curl -H "Authorization: Bearer valid-token" http://localhost:8080/api/protected

Custom Error Handling

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"

    "github.com/eatmoreapple/hx"
    . "github.com/eatmoreapple/hx/httpx"
)

// Custom error types
type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Details string `json:"details,omitempty"`
}

func (e APIError) Error() string {
    return e.Message
}

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("validation error on field %s: %s", e.Field, e.Message)
}

// Custom error handler
func customErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
    w.Header().Set("Content-Type", "application/json")

    var statusCode int
    var response interface{}

    switch e := err.(type) {
    case APIError:
        statusCode = e.Code
        response = e
    case ValidationError:
        statusCode = http.StatusBadRequest
        response = map[string]interface{}{
            "error": "validation_error",
            "field": e.Field,
            "message": e.Message,
        }
    default:
        statusCode = http.StatusInternalServerError
        response = map[string]interface{}{
            "error": "internal_error",
            "message": "An internal error occurred",
        }
    }

    w.WriteHeader(statusCode)
    json.NewEncoder(w).Encode(response)
}

func successHandler(ctx context.Context, req Empty) (string, error) {
    return "Success!", nil
}

func apiErrorHandler(ctx context.Context, req Empty) (string, error) {
    return "", APIError{
        Code:    http.StatusNotFound,
        Message: "Resource not found",
        Details: "The requested resource could not be found",
    }
}

func validationErrorHandler(ctx context.Context, req Empty) (string, error) {
    return "", ValidationError{
        Field:   "email",
        Message: "Invalid email format",
    }
}

func panicHandler(ctx context.Context, req Empty) (string, error) {
    return "", fmt.Errorf("something went wrong")
}

func main() {
    router := hx.New(hx.WithErrorHandler(customErrorHandler))

    router.GET("/success", hx.E(successHandler).String())
    router.GET("/api-error", hx.E(apiErrorHandler).String())
    router.GET("/validation-error", hx.E(validationErrorHandler).String())
    router.GET("/panic", hx.E(panicHandler).String())

    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", router)
}

JWT Authentication Example

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "strings"
    "time"

    "github.com/eatmoreapple/hx"
    . "github.com/eatmoreapple/hx/httpx"
)

// Simple JWT-like implementation (use a real JWT library in production)
type Claims struct {
    UserID   string    `json:"user_id"`
    Username string    `json:"username"`
    IssuedAt time.Time `json:"issued_at"`
}

func generateToken(userID, username string) string {
    claims := Claims{
        UserID:   userID,
        Username: username,
        IssuedAt: time.Now(),
    }
    data, _ := json.Marshal(claims)
    return string(data) // In production, use proper JWT signing
}

func parseToken(token string) (*Claims, error) {
    var claims Claims
    err := json.Unmarshal([]byte(token), &claims)
    if err != nil {
        return nil, fmt.Errorf("invalid token")
    }

    // Check token age (24 hours)
    if time.Since(claims.IssuedAt) > 24*time.Hour {
        return nil, fmt.Errorf("token expired")
    }

    return &claims, nil
}

// Request types
type LoginRequest struct {
    Username string `json:"username" form:"username"`
    Password string `json:"password" form:"password"`
}

type LoginResponse struct {
    Token    string `json:"token"`
    Username string `json:"username"`
}

// Context key for user claims
type contextKey string
const userClaimsKey contextKey = "user_claims"

// JWT middleware
func jwtMiddleware(next hx.HandlerFunc) hx.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) error {
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            return fmt.Errorf("authorization header required")
        }

        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            return fmt.Errorf("invalid authorization header format")
        }

        claims, err := parseToken(parts[1])
        if err != nil {
            return fmt.Errorf("invalid token: %v", err)
        }

        // Add claims to context
        ctx := context.WithValue(r.Context(), userClaimsKey, claims)
        r = r.WithContext(ctx)

        return next(w, r)
    }
}

func login(ctx context.Context, req LoginRequest) (LoginResponse, error) {
    // Simple authentication (use proper password hashing in production)
    if req.Username == "" || req.Password == "" {
        return LoginResponse{}, fmt.Errorf("username and password required")
    }

    if req.Username != "admin" || req.Password != "password" {
        return LoginResponse{}, fmt.Errorf("invalid credentials")
    }

    token := generateToken("1", req.Username)
    return LoginResponse{
        Token:    token,
        Username: req.Username,
    }, nil
}

func profile(ctx context.Context, req Empty) (map[string]interface{}, error) {
    claims := ctx.Value(userClaimsKey).(*Claims)
    return map[string]interface{}{
        "user_id":  claims.UserID,
        "username": claims.Username,
        "message":  "This is your profile",
    }, nil
}

func main() {
    router := hx.New()

    // Public route
    router.POST("/login", hx.G(login).JSON())

    // Protected routes
    protected := router.Group("/api")
    protected.Use(jwtMiddleware)
    protected.GET("/profile", hx.E(profile).JSON())

    fmt.Println("Server starting on :8080")
    fmt.Println("Login with: curl -X POST http://localhost:8080/login -d '{\"username\":\"admin\",\"password\":\"password\"}' -H 'Content-Type: application/json'")
    http.ListenAndServe(":8080", router)
}