Files
cv-site/doc/_go-learning/patterns/02-handler-pattern.md
T
juanatsap d95c62bad4 refactor: remove outdated server design documentation
Remove 557-line server-design.md from _go-learning/architecture - content is now covered in updated architecture documentation with real implementation examples and test coverage.
2025-12-02 20:25:05 +00:00

13 KiB

Handler Pattern in Go

Pattern Overview

The Handler Pattern organizes HTTP endpoint logic into structured, testable components. This project uses a method-based handler approach where related endpoints are grouped as methods on a handler struct.

Pattern Structure

// Handler struct holds dependencies
type Handler struct {
    tmpl *templates.Manager
    db   *database.DB
    // other dependencies
}

// Constructor with dependency injection
func NewHandler(tmpl *templates.Manager, db *database.DB) *Handler {
    return &Handler{
        tmpl: tmpl,
        db:   db,
    }
}

// HTTP handler methods
func (h *Handler) MethodName(w http.ResponseWriter, r *http.Request) {
    // Handle request
}

Real Implementation from Project

CVHandler Structure

// internal/handlers/cv.go

// CVHandler handles CV-related HTTP requests
type CVHandler struct {
    tmpl *templates.Manager  // Template renderer
    host string              // Server host for absolute URLs
}

// NewCVHandler creates a new CV handler with dependencies
func NewCVHandler(tmpl *templates.Manager, host string) *CVHandler {
    return &CVHandler{
        tmpl: tmpl,
        host: host,
    }
}

Page Handlers

// internal/handlers/cv_pages.go

// Home renders the main CV page
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
    // Get user preferences from context (set by middleware)
    prefs := middleware.GetPreferences(r)

    // Get language from query params, fallback to preference
    lang := r.URL.Query().Get("lang")
    if lang == "" {
        lang = prefs.CVLanguage
    }

    // Validate language
    if err := validateLanguage(lang); err != nil {
        h.HandleError(w, r, err)
        return
    }

    // Prepare template data
    data, err := h.prepareTemplateData(lang)
    if err != nil {
        h.HandleError(w, r, err)
        return
    }

    // Render template
    if err := h.tmpl.Render(w, "index.html", data); err != nil {
        h.HandleError(w, r, TemplateError(err))
        return
    }
}

// CVContent renders just the CV content (for HTMX partial updates)
func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
    prefs := middleware.GetPreferences(r)
    lang := prefs.CVLanguage

    data, err := h.prepareTemplateData(lang)
    if err != nil {
        h.HandleError(w, r, err)
        return
    }

    if err := h.tmpl.Render(w, "partials/cv_content.html", data); err != nil {
        h.HandleError(w, r, TemplateError(err))
        return
    }
}

HTMX Toggle Handlers

// internal/handlers/cv_htmx.go

// ToggleCVLength toggles between short and long CV formats
func (h *CVHandler) ToggleCVLength(w http.ResponseWriter, r *http.Request) {
    // Get current preferences from context
    prefs := middleware.GetPreferences(r)
    currentLength := prefs.CVLength

    // Toggle state
    newLength := "long"
    if currentLength == "long" {
        newLength = "short"
    }

    // Save new preference
    middleware.SetPreferenceCookie(w, "cv-length", newLength)

    // Render updated content
    lang := middleware.GetLanguage(r)
    data, err := h.prepareTemplateData(lang)
    if err != nil {
        h.HandleError(w, r, err)
        return
    }

    if err := h.tmpl.Render(w, "partials/cv_content.html", data); err != nil {
        h.HandleError(w, r, TemplateError(err))
        return
    }
}

// ToggleCVIcons toggles icon visibility
func (h *CVHandler) ToggleCVIcons(w http.ResponseWriter, r *http.Request) {
    // Similar pattern: get → toggle → save → render
    // ...
}

Helper Methods

// internal/handlers/cv_helpers.go

// prepareTemplateData loads and prepares all data for template rendering
func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) {
    // Load CV data
    cv, err := cvmodel.LoadCV(lang)
    if err != nil {
        return nil, DataNotFoundError("CV", lang).WithErr(err)
    }

    // Load UI strings
    ui, err := uimodel.LoadUI(lang)
    if err != nil {
        return nil, DataNotFoundError("UI", lang).WithErr(err)
    }

    // Calculate experience durations
    for i := range cv.Experience {
        cv.Experience[i].Duration = calculateDuration(
            cv.Experience[i].StartDate,
            cv.Experience[i].EndDate,
        )
    }

    // Split skills into columns
    skillColumns := splitSkillsIntoColumns(cv.Skills.Technical, 3)

    // Build data map
    return map[string]interface{}{
        "CV":            cv,
        "UI":            ui,
        "SkillsColumns": skillColumns,
        "PageTitle":     fmt.Sprintf("%s - %s", cv.Personal.Name, cv.Personal.Title),
        "CanonicalURL":  h.getFullURL("/"),
    }, nil
}

// getFullURL builds absolute URLs for SEO
func (h *CVHandler) getFullURL(path string) string {
    return fmt.Sprintf("http://%s%s", h.host, path)
}

Handler Organization by File

Separation of Concerns

internal/handlers/
├── cv.go              Constructor, shared state
├── cv_pages.go        Full page renders (Home, CVContent)
├── cv_htmx.go         HTMX partial updates (4 toggles)
├── cv_pdf.go          PDF export endpoint
├── cv_helpers.go      Shared utilities
├── types.go           Request/response types
└── errors.go          Error handling

This separation provides:

  1. Clear boundaries: Each file has a specific purpose
  2. Easier navigation: Find code by responsibility
  3. Better testing: Test files mirror source files
  4. Reduced conflicts: Multiple developers can work in parallel

Route Registration

// internal/routes/routes.go

func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) http.Handler {
    mux := http.NewServeMux()

    // Page routes
    mux.HandleFunc("/", cvHandler.Home)
    mux.HandleFunc("/cv", cvHandler.CVContent)

    // HTMX toggle routes
    mux.HandleFunc("/toggle/length", cvHandler.ToggleCVLength)
    mux.HandleFunc("/toggle/icons", cvHandler.ToggleCVIcons)
    mux.HandleFunc("/toggle/theme", cvHandler.ToggleCVTheme)
    mux.HandleFunc("/toggle/language", cvHandler.ToggleLanguage)

    // PDF export route (with additional middleware)
    pdfHandler := middleware.OriginChecker(
        middleware.RateLimiter(
            http.HandlerFunc(cvHandler.ExportPDF),
            3, // 3 requests per minute
        ),
    )
    mux.Handle("/export/pdf", pdfHandler)

    // Health check
    mux.HandleFunc("/health", healthHandler.Health)

    // Static files
    fs := http.FileServer(http.Dir("static"))
    mux.Handle("/static/", http.StripPrefix("/static/", fs))

    // Apply global middleware
    handler := middleware.Recovery(
        middleware.Logger(
            middleware.SecurityHeaders(
                middleware.PreferencesMiddleware(mux),
            ),
        ),
    )

    return handler
}

Handler Benefits

1. Dependency Injection

// Dependencies are explicit and injectable
type CVHandler struct {
    tmpl *templates.Manager  // Can be mocked
    db   *database.DB        // Can be mocked
    cache *redis.Client      // Can be mocked
}

// Easy to test with mocks
func TestHome(t *testing.T) {
    mockTmpl := &MockTemplateManager{}
    handler := NewCVHandler(mockTmpl, "localhost:8080")
    // Test with mock
}

2. Shared Logic

// Helpers available to all handler methods
func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) {
    // Reused by Home(), CVContent(), ToggleCVLength(), etc.
}

func (h *CVHandler) HandleError(w http.ResponseWriter, r *http.Request, err error) {
    // Centralized error handling for all methods
}

3. Context Access

// All handler methods have access to:
// - Dependencies (h.tmpl, h.host)
// - Request (r)
// - Response (w)
func (h *CVHandler) AnyMethod(w http.ResponseWriter, r *http.Request) {
    // Can access h.tmpl, h.host, etc.
}

Alternative Handler Patterns

1. Function-Based Handlers

// Simple approach for small apps
func Home(w http.ResponseWriter, r *http.Request) {
    // No struct, just a function
    // Dependencies passed as globals or closures
}

When to use: Very small apps, simple endpoints Drawbacks: Hard to test, shared logic difficult, no dependency injection

2. Handler with Interface

// Interface-based approach
type Handler interface {
    Home(w http.ResponseWriter, r *http.Request)
    Profile(w http.ResponseWriter, r *http.Request)
}

type CVHandler struct {
    // ...
}

func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
    // ...
}

When to use: Multiple implementations, complex testing Drawbacks: More boilerplate, potentially over-engineered

3. Handler with http.Handler Interface

// Implement http.Handler interface directly
type HomeHandler struct {
    tmpl *templates.Manager
}

func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Handle request
}

// Register
mux.Handle("/", &HomeHandler{tmpl: tmplManager})

When to use: When you need to pass handlers around as interfaces Drawbacks: One handler per endpoint, lots of small types

Testing Handlers

Unit Test Example

// internal/handlers/cv_pages_test.go

func TestHome(t *testing.T) {
    // Setup
    cfg := &config.TemplateConfig{
        Dir:         "../../templates",
        PartialsDir: "../../templates/partials",
        HotReload:   true,
    }
    tmplManager, err := templates.NewManager(cfg)
    if err != nil {
        t.Fatal(err)
    }

    handler := NewCVHandler(tmplManager, "localhost:8080")

    // Create test request
    req := httptest.NewRequest(http.MethodGet, "/?lang=en", nil)
    w := httptest.NewRecorder()

    // Execute
    handler.Home(w, req)

    // Verify
    if w.Code != http.StatusOK {
        t.Errorf("expected 200, got %d", w.Code)
    }

    body := w.Body.String()
    if !strings.Contains(body, "<!DOCTYPE html>") {
        t.Error("response should be HTML")
    }
}

Table-Driven Tests

func TestHome(t *testing.T) {
    tests := []struct {
        name       string
        lang       string
        wantStatus int
        wantBody   string
    }{
        {
            name:       "English version",
            lang:       "en",
            wantStatus: http.StatusOK,
            wantBody:   "Professional Summary",
        },
        {
            name:       "Spanish version",
            lang:       "es",
            wantStatus: http.StatusOK,
            wantBody:   "Resumen Profesional",
        },
        {
            name:       "Invalid language",
            lang:       "xx",
            wantStatus: http.StatusBadRequest,
            wantBody:   "INVALID_LANGUAGE",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest(http.MethodGet, "/?lang="+tt.lang, nil)
            w := httptest.NewRecorder()

            handler.Home(w, req)

            if w.Code != tt.wantStatus {
                t.Errorf("status = %d, want %d", w.Code, tt.wantStatus)
            }

            if !strings.Contains(w.Body.String(), tt.wantBody) {
                t.Errorf("body missing %q", tt.wantBody)
            }
        })
    }
}

Best Practices

DO

// Keep handlers focused on HTTP concerns
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
    // Parse request
    // Validate input
    // Call business logic
    // Render response
}

// Extract business logic to helpers
func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) {
    // This can be tested independently
}

// Use dependency injection
func NewCVHandler(tmpl *templates.Manager, host string) *CVHandler {
    return &CVHandler{tmpl: tmpl, host: host}
}

// Group related handlers
type CVHandler struct {
    // CV-related endpoints
}
type UserHandler struct {
    // User-related endpoints
}

DON'T

// DON'T put business logic in handlers
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
    // 500 lines of business logic here...
}

// DON'T use global state
var globalTemplateManager *templates.Manager

// DON'T mix unrelated endpoints
type Handler struct {
    // CV, Users, Orders, Payments all in one struct
}

// DON'T ignore errors
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
    data, _ := h.prepareTemplateData(lang)  // Ignoring error!
    h.tmpl.Render(w, "index.html", data)
}

Handler Testing Checklist

  • Test happy path
  • Test invalid input
  • Test missing data
  • Test error handling
  • Test with different preferences/context
  • Test response headers
  • Test response status codes
  • Test response body content
  • Dependency Injection: Used in handler constructors
  • Middleware Pattern: Wraps handlers for cross-cutting concerns
  • Context Pattern: Request-scoped values in handlers
  • Error Wrapping: Structured error handling in handlers

Further Reading