# Backend Handler Architecture **Last Updated**: November 20, 2024 ## Overview This document explains how the backend handles HTTP requests, focusing on the handler architecture, type safety, middleware pattern, and testing strategy implemented in the CV website. ## Table of Contents 1. [Handler Architecture](#handler-architecture) 2. [Request/Response Types](#requestresponse-types) 3. [Middleware Pattern](#middleware-pattern) 4. [Testing Strategy](#testing-strategy) 5. [Data Flow](#data-flow) 6. [Best Practices](#best-practices) --- ## Handler Architecture ### File Organization The handler layer is organized by responsibility into focused files: ``` internal/handlers/ ├── cv.go # Core handler struct and constructor ├── cv_pages.go # Page rendering handlers ├── cv_pdf.go # PDF export handler ├── cv_htmx.go # HTMX toggle handlers ├── cv_helpers.go # Shared helper functions ├── types.go # Request/response types ├── errors.go # Error handling utilities └── *_test.go # Comprehensive test suites ``` ### Handler Responsibilities #### 1. Page Handlers (cv_pages.go) **Purpose**: Render full HTML pages and content sections ```go // Home - Renders the complete CV page with all content func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) // CVContent - Renders CV content for HTMX swaps func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) // DefaultCVShortcut - Handles shortcut URLs like /cv-jamr-2025-en.pdf func (h *CVHandler) DefaultCVShortcut(w http.ResponseWriter, r *http.Request) ``` **Example Flow**: ``` Browser Request → Home() → prepareTemplateData() → Render HTML → Response ``` #### 2. PDF Handler (cv_pdf.go) **Purpose**: Generate PDF exports with customizable options ```go // ExportPDF - Generates PDF with parameters: lang, length, icons, version func (h *CVHandler) ExportPDF(w http.ResponseWriter, r *http.Request) ``` **Features**: - Multi-language support (English, Spanish) - Length variants (short, long) - Icon visibility toggle (show, hide) - Theme variants (default with skills, clean without skills) - Smart filename generation - Print-optimized CSS rendering **Example Request**: ```bash GET /export-pdf?lang=es&length=long&icons=show&version=with_skills ``` #### 3. HTMX Toggle Handlers (cv_htmx.go) **Purpose**: Handle interactive toggles via HTMX ```go // ToggleLength - Toggle between short and long CV func (h *CVHandler) ToggleLength(w http.ResponseWriter, r *http.Request) // ToggleIcons - Show/hide skill and tool icons func (h *CVHandler) ToggleIcons(w http.ResponseWriter, r *http.Request) // SwitchLanguage - Switch between English and Spanish func (h *CVHandler) SwitchLanguage(w http.ResponseWriter, r *http.Request) // ToggleTheme - Toggle between default and clean theme func (h *CVHandler) ToggleTheme(w http.ResponseWriter, r *http.Request) ``` **HTMX Pattern**: 1. User clicks toggle button 2. HTMX sends POST request 3. Handler updates cookie 4. Handler returns HTML fragment with out-of-band swaps 5. HTMX swaps multiple DOM elements atomically --- ## Request/Response Types ### Type-Safe Request Handling Instead of manually parsing query parameters, we use structured types with validation: #### PDF Export Request ```go // PDFExportRequest represents all PDF export parameters type PDFExportRequest struct { Lang string // "en" or "es" Length string // "short" or "long" Icons string // "show" or "hide" Version string // "with_skills" or "clean" } // Parse and validate in one call req, err := ParsePDFExportRequest(r) if err != nil { // Return 400 Bad Request with clear error message HandleError(w, r, BadRequestError(err.Error())) return } // Type-safe access filename := fmt.Sprintf("cv-%s-%s.pdf", req.Length, req.Lang) ``` #### Benefits ✅ **Type Safety**: Compile-time guarantees prevent typos ✅ **Self-Documenting**: Struct fields show all valid parameters ✅ **Centralized Validation**: One place to update validation rules ✅ **Clear Errors**: Descriptive error messages for invalid requests **Example Validation**: ```go // Automatic validation with helpful error messages GET /export-pdf?lang=fr → 400 Bad Request: "unsupported language: fr (use 'en' or 'es')" GET /export-pdf?length=medium → 400 Bad Request: "unsupported length: medium (use 'short' or 'long')" ``` #### Language Request ```go // LanguageRequest for endpoints that only need language type LanguageRequest struct { Lang string // "en" or "es" } // Usage req, err := ParseLanguageRequest(r) // Defaults to "en" if not specified // Validates against supported languages ``` --- ## Middleware Pattern ### Preferences Middleware **Purpose**: Read user preferences from cookies once and make them available via context #### Architecture ``` Request ↓ PreferencesMiddleware ├─ Read all preference cookies ├─ Migrate old values (extended → long, true → show) ├─ Store in request context └─ Pass to next handler ↓ Handler ├─ Get preferences from context ├─ No cookie reading needed └─ Use preferences in business logic ↓ Response ``` #### Implementation ```go // Middleware reads cookies and stores in context func PreferencesMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { prefs := &Preferences{ CVLength: getPreferenceCookie(r, "cv-length", "short"), CVIcons: getPreferenceCookie(r, "cv-icons", "show"), CVLanguage: getPreferenceCookie(r, "cv-language", "en"), CVTheme: getPreferenceCookie(r, "cv-theme", "default"), ColorTheme: getPreferenceCookie(r, "color-theme", "light"), } // Automatic migration of old preference values if prefs.CVLength == "extended" { prefs.CVLength = "long" } // Store in context for handlers ctx := context.WithValue(r.Context(), PreferencesKey, prefs) next.ServeHTTP(w, r.WithContext(ctx)) }) } // Handlers access preferences via context func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) { // Get preferences from context (already read by middleware) prefs := middleware.GetPreferences(r) // Use preferences cvLengthClass := "cv-short" if prefs.CVLength == "long" { cvLengthClass = "cv-long" } } ``` #### Benefits ✅ **Performance**: Cookies read once per request ✅ **Consistency**: All handlers get same preference values ✅ **Maintainability**: Migration logic in one place ✅ **Testability**: Easy to mock preferences via context --- ## Testing Strategy ### Test Coverage The handler layer has comprehensive test coverage across multiple files: ``` internal/handlers/ ├── cv_pages_test.go # Page handler tests ├── cv_htmx_test.go # HTMX toggle tests ├── pdf_test.go # PDF generation tests (integration) └── cv_security_test.go # Security validation tests ``` ### Page Handler Tests **File**: `cv_pages_test.go` **Test Cases**: 15+ **Coverage**: Language validation, rendering, shortcuts ```go // Example test structure func TestHome(t *testing.T) { tests := []struct { name string lang string expectStatus int expectContains string }{ { name: "Default language (English)", lang: "", expectStatus: http.StatusOK, expectContains: "Juan Andrés Moreno Rubio", }, { name: "Invalid language", lang: "fr", expectStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Test implementation }) } } ``` ### HTMX Handler Tests **File**: `cv_htmx_test.go` **Test Cases**: 20+ **Coverage**: Toggles, cookies, method validation, migrations ```go // Example: Testing toggle behavior func TestToggleLength(t *testing.T) { tests := []struct { name string currentLength string expectedToggle string }{ { name: "Toggle from short to long", currentLength: "short", expectedToggle: "long", }, { name: "Migration: extended → long", currentLength: "extended", expectedToggle: "short", // extended becomes long, then toggles }, } // ... } ``` ### Method Validation Tests All HTMX endpoints enforce POST-only requests: ```go func TestHTMXHandlersRequirePost(t *testing.T) { // Tests verify GET requests return 405 Method Not Allowed handlers := []struct { name string handler func(http.ResponseWriter, *http.Request) }{ {"ToggleLength", handler.ToggleLength}, {"ToggleIcons", handler.ToggleIcons}, {"ToggleTheme", handler.ToggleTheme}, } // All should reject GET with 405 for _, h := range handlers { req := httptest.NewRequest(http.MethodGet, "/endpoint", nil) w := httptest.NewRecorder() h.handler(w, req) assert.Equal(t, http.StatusMethodNotAllowed, w.Code) } } ``` ### Running Tests ```bash # Run all unit tests (excludes PDF generation) go test -short ./... # Run specific handler tests go test -short ./internal/handlers/... -v # Run all tests including integration tests make test-all # Pre-commit hook runs tests automatically git commit -m "changes" # Tests run before commit ``` --- ## Data Flow ### Request Processing Flow ``` 1. Client Request ├─ Browser/HTMX makes HTTP request └─ URL: /export-pdf?lang=es&length=long 2. Middleware Chain ├─ Recovery (catch panics) ├─ Logger (request logging) ├─ Security Headers (CSP, HSTS) └─ PreferencesMiddleware (read cookies) 3. Router ├─ Match URL pattern └─ Dispatch to handler 4. Handler ├─ Parse request (type-safe) │ └─ ParsePDFExportRequest(r) ├─ Validate parameters │ └─ Return 400 if invalid ├─ Prepare data │ └─ prepareTemplateData(lang) ├─ Generate response │ └─ Render template or generate PDF └─ Return response 5. Client Response ├─ HTML page ├─ HTMX fragment ├─ PDF download └─ Error page ``` ### Template Data Preparation Central helper function used by multiple handlers: ```go func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) { // Load CV data (cached) cv, err := cvmodel.LoadCV(lang) if err != nil { return nil, err } // Load UI translations (cached) ui, err := uimodel.LoadUI(lang) if err != nil { return nil, err } // Calculate dynamic data for i := range cv.Experience { cv.Experience[i].Duration = calculateDuration( cv.Experience[i].StartDate, cv.Experience[i].EndDate, cv.Experience[i].Current, lang, ) } // Process projects for i := range cv.Projects { processProjectDates(&cv.Projects[i], lang) } // Prepare skills skillsLeft, skillsRight := splitSkills(cv.Skills.Technical) // Return complete data map return map[string]interface{}{ "CV": cv, "UI": ui, "Lang": lang, "SkillsLeft": skillsLeft, "SkillsRight": skillsRight, "YearsOfExperience": calculateYearsOfExperience(), "CurrentYear": time.Now().Year(), "CanonicalURL": fmt.Sprintf("https://juan.andres.morenorub.io/?lang=%s", lang), "AlternateEN": "https://juan.andres.morenorub.io/?lang=en", "AlternateES": "https://juan.andres.morenorub.io/?lang=es", }, nil } ``` --- ## Best Practices ### 1. Type-Safe Requests ✅ **DO**: Use structured request types ```go req, err := ParsePDFExportRequest(r) if err != nil { HandleError(w, r, BadRequestError(err.Error())) return } ``` ❌ **DON'T**: Manually parse parameters ```go lang := r.URL.Query().Get("lang") if lang == "" { lang = "en" } if lang != "en" && lang != "es" { // Repetitive validation code } ``` ### 2. Centralized Validation ✅ **DO**: Validate in request parser ```go func ParsePDFExportRequest(r *http.Request) (*PDFExportRequest, error) { req := &PDFExportRequest{ /* parse */ } // All validation in one place if req.Lang != "en" && req.Lang != "es" { return nil, fmt.Errorf("unsupported language: %s", req.Lang) } return req, nil } ``` ❌ **DON'T**: Scatter validation across handlers ```go // Validation duplicated in multiple places ``` ### 3. Reuse Helper Functions ✅ **DO**: Use shared data preparation ```go func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) { data, err := h.prepareTemplateData(lang) // Add page-specific fields } func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) { data, err := h.prepareTemplateData(lang) // Reuse same data preparation } ``` ❌ **DON'T**: Duplicate data preparation logic ```go // 100+ lines duplicated across handlers ``` ### 4. Test All Handlers ✅ **DO**: Write comprehensive tests ```go func TestToggleLength(t *testing.T) { // Test toggle behavior // Test cookie persistence // Test migration from old values } ``` ✅ **DO**: Test error cases ```go func TestInvalidLanguage(t *testing.T) { // Verify 400 Bad Request // Check error message } ``` ### 5. Use Middleware for Cross-Cutting Concerns ✅ **DO**: Extract common logic to middleware ```go // PreferencesMiddleware reads cookies once // Handlers get preferences from context ``` ❌ **DON'T**: Read cookies in every handler ```go // Cookie reading duplicated across handlers ``` --- ## Related Documentation - [Architecture Overview](./1-ARCHITECTURE.md) - System architecture patterns - [API Reference](./3-API.md) - Complete API documentation - [Security](./9-SECURITY.md) - Security implementation details - [PDF Export](./11-PDF-EXPORT.md) - PDF generation details - [Testing Guide](../_go-learning/refactorings/) - Detailed refactoring documentation --- ## Changelog - **Nov 20, 2024**: Initial documentation covering handler refactoring, type safety, middleware pattern, and testing strategy