# 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) 7. [Architectural Enhancements](#architectural-enhancements) - [Response Types](#response-types) - [Validation Tags](#validation-tags) - [Context Helpers](#context-helpers) - [Typed Errors](#typed-errors) - [Performance Benchmarks](#performance-benchmarks) --- ## 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 ``` --- ## Architectural Enhancements ### Response Types The handler layer uses standardized response types for consistent API responses: ```go // APIResponse - Standardized wrapper for JSON responses type APIResponse struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error *ErrorInfo `json:"error,omitempty"` Meta *MetaInfo `json:"meta,omitempty"` } // ErrorInfo - Structured error information type ErrorInfo struct { Code string `json:"code"` // Error code Message string `json:"message"` // Human-readable message Field string `json:"field,omitempty"` // Field that caused error Details string `json:"details,omitempty"` // Additional details } ``` **Helper Functions**: - `SuccessResponse(data)` - Create success response - `NewErrorResponse(code, message)` - Create error response - `ErrorResponseWithField(code, message, field)` - Error with field info ### Validation Tags Request types use struct tags for declarative validation: ```go type PDFExportRequest struct { Lang string `validate:"required,oneof=en es"` Length string `validate:"required,oneof=short long"` Icons string `validate:"required,oneof=show hide"` Version string `validate:"required,oneof=with_skills clean"` } ``` **Benefits**: - Self-documenting validation rules - Ready for validator library integration - Centralized validation logic - Easy to extend ### Context Helpers The middleware provides 13 convenience functions for cleaner code: ```go // Getters middleware.GetLanguage(r) // Get language preference middleware.GetCVLength(r) // Get CV length preference middleware.GetCVTheme(r) // Get theme preference // Boolean helpers middleware.IsLongCV(r) // True if long CV format middleware.ShowIcons(r) // True if icons visible middleware.IsCleanTheme(r) // True if clean theme middleware.IsDarkMode(r) // True if dark mode ``` **Usage**: ```go // Before prefs := middleware.GetPreferences(r) if prefs.CVLength == "long" { // ... } // After if middleware.IsLongCV(r) { // ... } ``` ### Typed Errors Domain-specific errors with error codes for programmatic handling: ```go // Error codes type ErrorCode string const ( ErrCodeInvalidLanguage ErrorCode = "INVALID_LANGUAGE" ErrCodeInvalidLength ErrorCode = "INVALID_LENGTH" ErrCodePDFGeneration ErrorCode = "PDF_GENERATION" ErrCodeRateLimitExceeded ErrorCode = "RATE_LIMIT_EXCEEDED" // ... 13 total error codes ) // DomainError with context type DomainError struct { Code ErrorCode Message string Err error StatusCode int Field string } ``` **Constructors**: ```go InvalidLanguageError(lang) // Returns typed error with code PDFGenerationError(err) // Wraps underlying error RateLimitError() // Rate limit exceeded ``` **Usage**: ```go // Create typed error return InvalidLanguageError("fr") // Returns: INVALID_LANGUAGE: Unsupported language: fr (use 'en' or 'es') // Error chaining return PDFGenerationError(err).WithError(originalErr) ``` ### Performance Benchmarks Comprehensive benchmark suite for performance monitoring: **Handlers** (11 benchmarks): - `BenchmarkHome` - Home page handler - `BenchmarkCVContent` - Content rendering - `BenchmarkToggleLength` - Toggle handlers - `BenchmarkParsePDFExportRequest` - Request parsing - `BenchmarkPrepareTemplateData` - Data preparation - `BenchmarkParallelHome` - Parallel load test - Response creation benchmarks **Middleware** (12 benchmarks): - `BenchmarkPreferencesMiddleware` - Middleware performance - `BenchmarkGetPreferences` - Context retrieval - `BenchmarkGetLanguage` - Helper functions - `BenchmarkIsLongCV` - Boolean helpers - `BenchmarkParallelPreferencesMiddleware` - Concurrent load **Running Benchmarks**: ```bash # All benchmarks go test -bench=. ./internal/handlers/... ./internal/middleware/... # Specific benchmark with memory stats go test -bench=BenchmarkHome -benchmem ./internal/handlers/... # Compare for regression detection go test -bench=. -benchmem ./... > baseline.txt # Make changes go test -bench=. -benchmem ./... > current.txt benchcmp baseline.txt current.txt ``` --- ## 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**: Added architectural enhancements section (response types, validation tags, context helpers, typed errors, benchmarks) - **Nov 20, 2024**: Initial documentation covering handler refactoring, type safety, middleware pattern, and testing strategy