# 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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, "") { t.Error("response should be HTML") } } ``` ### Table-Driven Tests ```go 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 ```go // 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 ```go // 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 ## Related Patterns - **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 - [HTTP Handler Pattern](https://www.alexedwards.net/blog/a-recap-of-request-handling) - [Structuring Go Applications](https://www.gobeyond.dev/standard-package-layout/) - [Dependency Injection](./05-dependency-injection.md)