Advanced Featuresο
This section covers advanced HX features for building sophisticated applications.
Custom Request Extractorsο
You can create custom extractors by implementing the RequestExtractor interface:
package main
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/eatmoreapple/hx"
. "github.com/eatmoreapple/hx/httpx"
)
// Custom extractor for pagination
type PaginationExtractor struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int `json:"total"`
}
func (p *PaginationExtractor) FromRequest(r *http.Request) error {
// Set defaults
p.Page = 1
p.Limit = 10
// Extract page
if pageStr := r.URL.Query().Get("page"); pageStr != "" {
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
p.Page = page
}
}
// Extract limit
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 && limit <= 100 {
p.Limit = limit
}
}
return nil
}
// Custom extractor for search filters
type SearchFilters struct {
Query string `json:"query"`
Tags []string `json:"tags"`
Category string `json:"category"`
SortBy string `json:"sort_by"`
SortDesc bool `json:"sort_desc"`
}
func (s *SearchFilters) FromRequest(r *http.Request) error {
query := r.URL.Query()
s.Query = query.Get("q")
s.Category = query.Get("category")
s.SortBy = query.Get("sort_by")
if s.SortBy == "" {
s.SortBy = "created_at"
}
s.SortDesc = query.Get("order") == "desc"
// Parse tags from comma-separated values
if tagsStr := query.Get("tags"); tagsStr != "" {
s.Tags = strings.Split(tagsStr, ",")
// Trim whitespace
for i, tag := range s.Tags {
s.Tags[i] = strings.TrimSpace(tag)
}
}
return nil
}
// Request type using custom extractors
type SearchRequest struct {
Pagination PaginationExtractor `json:"pagination"`
Filters SearchFilters `json:"filters"`
}
func searchHandler(ctx context.Context, req SearchRequest) (map[string]interface{}, error) {
return map[string]interface{}{
"pagination": req.Pagination,
"filters": req.Filters,
"results": []string{"item1", "item2", "item3"}, // Mock results
}, nil
}
func main() {
router := hx.New()
router.GET("/search", hx.G(searchHandler).JSON())
fmt.Println("Server starting on :8080")
fmt.Println("Try: http://localhost:8080/search?q=golang&tags=web,api&category=tutorial&page=2&limit=5&sort_by=title&order=desc")
http.ListenAndServe(":8080", router)
}
Custom Response Typesο
Implement the ResponseRender interface for custom response handling:
package main
import (
"context"
"encoding/csv"
"fmt"
"net/http"
"strconv"
"github.com/eatmoreapple/hx"
. "github.com/eatmoreapple/hx/httpx"
)
// CSV Response
type CSVResponse struct {
Headers []string
Rows [][]string
}
func (c CSVResponse) IntoResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", "attachment; filename=data.csv")
writer := csv.NewWriter(w)
defer writer.Flush()
// Write headers
if err := writer.Write(c.Headers); err != nil {
return err
}
// Write rows
for _, row := range c.Rows {
if err := writer.Write(row); err != nil {
return err
}
}
return nil
}
// PDF Response (simplified)
type PDFResponse struct {
Content []byte
Filename string
}
func (p PDFResponse) IntoResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "application/pdf")
if p.Filename != "" {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", p.Filename))
}
_, err := w.Write(p.Content)
return err
}
// Template Response
type TemplateResponse struct {
Template string
Data interface{}
}
func (t TemplateResponse) IntoResponse(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "text/html")
// Simple template rendering (use a real template engine in production)
html := fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head><title>%s</title></head>
<body>
<h1>%s</h1>
<pre>%+v</pre>
</body>
</html>`, t.Template, t.Template, t.Data)
_, err := w.Write([]byte(html))
return err
}
func csvHandler(ctx context.Context, req Empty) (CSVResponse, error) {
return CSVResponse{
Headers: []string{"ID", "Name", "Email"},
Rows: [][]string{
{"1", "John Doe", "john@example.com"},
{"2", "Jane Smith", "jane@example.com"},
{"3", "Bob Johnson", "bob@example.com"},
},
}, nil
}
func pdfHandler(ctx context.Context, req Empty) (PDFResponse, error) {
// Mock PDF content
content := []byte("%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n...")
return PDFResponse{
Content: content,
Filename: "report.pdf",
}, nil
}
func templateHandler(ctx context.Context, req Empty) (TemplateResponse, error) {
return TemplateResponse{
Template: "User Dashboard",
Data: map[string]interface{}{
"User": "John Doe",
"Time": "2025-01-01 12:00:00",
},
}, nil
}
func main() {
router := hx.New()
router.GET("/export/csv", hx.R(func(ctx context.Context, req Empty) (ResponseRender, error) {
return csvHandler(ctx, req)
}))
router.GET("/export/pdf", hx.R(func(ctx context.Context, req Empty) (ResponseRender, error) {
return pdfHandler(ctx, req)
}))
router.GET("/dashboard", hx.R(func(ctx context.Context, req Empty) (ResponseRender, error) {
return templateHandler(ctx, req)
}))
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", router)
}
Advanced Middleware Patternsο
Rate Limiting Middlewareο
package main
import (
"fmt"
"net/http"
"sync"
"time"
"github.com/eatmoreapple/hx"
)
type RateLimiter struct {
requests map[string][]time.Time
mutex sync.Mutex
limit int
window time.Duration
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
return &RateLimiter{
requests: make(map[string][]time.Time),
limit: limit,
window: window,
}
}
func (rl *RateLimiter) Middleware() hx.Middleware {
return func(next hx.HandlerFunc) hx.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
clientIP := r.RemoteAddr
rl.mutex.Lock()
defer rl.mutex.Unlock()
now := time.Now()
// Clean old requests
if requests, exists := rl.requests[clientIP]; exists {
filtered := requests[:0]
for _, reqTime := range requests {
if now.Sub(reqTime) < rl.window {
filtered = append(filtered, reqTime)
}
}
rl.requests[clientIP] = filtered
}
// Check limit
if len(rl.requests[clientIP]) >= rl.limit {
return fmt.Errorf("rate limit exceeded")
}
// Add current request
rl.requests[clientIP] = append(rl.requests[clientIP], now)
return next(w, r)
}
}
}
Circuit Breaker Middlewareο
package main
import (
"fmt"
"net/http"
"sync"
"time"
"github.com/eatmoreapple/hx"
)
type CircuitState int
const (
StateClosed CircuitState = iota
StateOpen
StateHalfOpen
)
type CircuitBreaker struct {
maxFailures int
resetTimeout time.Duration
state CircuitState
failures int
lastFailTime time.Time
mutex sync.Mutex
}
func NewCircuitBreaker(maxFailures int, resetTimeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
maxFailures: maxFailures,
resetTimeout: resetTimeout,
state: StateClosed,
}
}
func (cb *CircuitBreaker) Middleware() hx.Middleware {
return func(next hx.HandlerFunc) hx.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
cb.mutex.Lock()
// Check if we should reset
if cb.state == StateOpen && time.Since(cb.lastFailTime) > cb.resetTimeout {
cb.state = StateHalfOpen
cb.failures = 0
}
// Reject if circuit is open
if cb.state == StateOpen {
cb.mutex.Unlock()
return fmt.Errorf("circuit breaker is open")
}
cb.mutex.Unlock()
// Execute request
err := next(w, r)
cb.mutex.Lock()
defer cb.mutex.Unlock()
if err != nil {
cb.failures++
cb.lastFailTime = time.Now()
if cb.failures >= cb.maxFailures {
cb.state = StateOpen
}
} else if cb.state == StateHalfOpen {
cb.state = StateClosed
cb.failures = 0
}
return err
}
}
}
Request Context Enhancementο
package main
import (
"context"
"fmt"
"net/http"
"time"
"github.com/eatmoreapple/hx"
. "github.com/eatmoreapple/hx/httpx"
)
// Context keys
type contextKey string
const (
requestIDKey contextKey = "request_id"
userIDKey contextKey = "user_id"
traceIDKey contextKey = "trace_id"
)
// Request ID middleware
func requestIDMiddleware(next hx.HandlerFunc) hx.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = fmt.Sprintf("%d", time.Now().UnixNano())
}
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
r = r.WithContext(ctx)
w.Header().Set("X-Request-ID", requestID)
return next(w, r)
}
}
// User context middleware
func userContextMiddleware(next hx.HandlerFunc) hx.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
userID := r.Header.Get("X-User-ID")
if userID != "" {
ctx := context.WithValue(r.Context(), userIDKey, userID)
r = r.WithContext(ctx)
}
return next(w, r)
}
}
func contextHandler(ctx context.Context, req Empty) (map[string]interface{}, error) {
response := make(map[string]interface{})
if requestID := ctx.Value(requestIDKey); requestID != nil {
response["request_id"] = requestID
}
if userID := ctx.Value(userIDKey); userID != nil {
response["user_id"] = userID
}
response["message"] = "Context data extracted successfully"
return response, nil
}
func main() {
router := hx.New()
router.Use(requestIDMiddleware, userContextMiddleware)
router.GET("/context", hx.E(contextHandler).JSON())
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", router)
}
Request Validationο
package main
import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
"github.com/eatmoreapple/hx"
. "github.com/eatmoreapple/hx/httpx"
)
// Validation interface
type Validator interface {
Validate() error
}
// Validation middleware
func validationMiddleware(next hx.HandlerFunc) hx.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
// This middleware would need to be applied at the handler level
// for access to the typed request
return next(w, r)
}
}
// User creation request with validation
type CreateUserRequest struct {
Name string `json:"name" form:"name"`
Email string `json:"email" form:"email"`
Password string `json:"password" form:"password"`
Age int `json:"age" form:"age"`
}
func (r CreateUserRequest) Validate() error {
var errors []string
// Name validation
if r.Name == "" {
errors = append(errors, "name is required")
} else if len(r.Name) < 2 {
errors = append(errors, "name must be at least 2 characters")
}
// Email validation
if r.Email == "" {
errors = append(errors, "email is required")
} else {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(r.Email) {
errors = append(errors, "invalid email format")
}
}
// Password validation
if r.Password == "" {
errors = append(errors, "password is required")
} else if len(r.Password) < 8 {
errors = append(errors, "password must be at least 8 characters")
}
// Age validation
if r.Age < 0 || r.Age > 150 {
errors = append(errors, "age must be between 0 and 150")
}
if len(errors) > 0 {
return fmt.Errorf("validation errors: %s", strings.Join(errors, ", "))
}
return nil
}
func createUserHandler(ctx context.Context, req CreateUserRequest) (map[string]interface{}, error) {
// Validate request
if err := req.Validate(); err != nil {
return nil, err
}
// Process valid request
return map[string]interface{}{
"message": "User created successfully",
"user": map[string]interface{}{
"name": req.Name,
"email": req.Email,
"age": req.Age,
},
}, nil
}
func main() {
router := hx.New()
router.POST("/users", hx.G(createUserHandler).JSON())
fmt.Println("Server starting on :8080")
fmt.Println("Test with: curl -X POST http://localhost:8080/users -H 'Content-Type: application/json' -d '{\"name\":\"John\",\"email\":\"john@example.com\",\"password\":\"password123\",\"age\":25}'")
http.ListenAndServe(":8080", router)
}
Database Integrationο
package main
import (
"context"
"database/sql"
"fmt"
"net/http"
"github.com/eatmoreapple/hx"
. "github.com/eatmoreapple/hx/httpx"
_ "github.com/mattn/go-sqlite3" // SQLite driver
)
type User struct {
ID int `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Email string `json:"email" db:"email"`
}
type UserService struct {
db *sql.DB
}
func NewUserService(db *sql.DB) *UserService {
return &UserService{db: db}
}
func (s *UserService) GetUser(id int) (*User, error) {
user := &User{}
err := s.db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id).
Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return user, nil
}
func (s *UserService) CreateUser(name, email string) (*User, error) {
result, err := s.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", name, email)
if err != nil {
return nil, err
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
}
return &User{
ID: int(id),
Name: name,
Email: email,
}, nil
}
// Dependency injection middleware
func serviceMiddleware(userService *UserService) hx.Middleware {
return func(next hx.HandlerFunc) hx.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
ctx := context.WithValue(r.Context(), "userService", userService)
r = r.WithContext(ctx)
return next(w, r)
}
}
}
type UserIDExtractor string
func (u UserIDExtractor) ValueName() string { return "id" }
type GetUserRequest struct {
ID FromPath[UserIDExtractor] `json:"id"`
}
type CreateUserRequest struct {
Name string `json:"name" form:"name"`
Email string `json:"email" form:"email"`
}
func getUserHandler(ctx context.Context, req GetUserRequest) (*User, error) {
userService := ctx.Value("userService").(*UserService)
id := 0 // Convert string to int (simplified)
fmt.Sscanf(string(req.ID), "%d", &id)
return userService.GetUser(id)
}
func createUserHandler(ctx context.Context, req CreateUserRequest) (*User, error) {
userService := ctx.Value("userService").(*UserService)
return userService.CreateUser(req.Name, req.Email)
}
func main() {
// Initialize database
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
defer db.Close()
// Create table
_, err = db.Exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)
`)
if err != nil {
panic(err)
}
userService := NewUserService(db)
router := hx.New()
router.Use(serviceMiddleware(userService))
router.GET("/users/{id}", hx.G(getUserHandler).JSON())
router.POST("/users", hx.G(createUserHandler).JSON())
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", router)
}