Files
cv-site/doc/14-BACKEND-HANDLERS.md
T
juanatsap c23068508f docs: Add comprehensive documentation for architectural enhancements
Created detailed documentation for all 5 architectural improvements:

Educational Documentation (_go-learning/):
- Created 005-architectural-enhancements.md (900+ lines)
- Detailed explanation of each enhancement
- Code examples and usage patterns
- Before/after comparisons
- Benefits and interview talking points
- Future considerations

Public Documentation (doc/):
- Updated 14-BACKEND-HANDLERS.md
- Added "Architectural Enhancements" section
- Response Types with examples
- Validation Tags guide
- Context Helpers usage
- Typed Errors documentation
- Performance Benchmarks guide
- Updated table of contents
- Updated changelog

Documentation Coverage:
- Response Types: Structure, helpers, usage examples
- Validation Tags: Declarative rules, self-documenting
- Context Helpers: 13 functions documented
- Typed Errors: 13 error codes, constructors, usage
- Benchmarks: 23 benchmarks, running instructions

All improvements now fully documented for:
- Internal learning and interviews
- Public consumption and contribution
- Developer onboarding
- Architecture understanding
2025-11-20 18:24:41 +00:00

19 KiB

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
  2. Request/Response Types
  3. Middleware Pattern
  4. Testing Strategy
  5. Data Flow
  6. Best Practices
  7. Architectural Enhancements

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

// 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

// 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:

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

// 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

// 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:

// 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

// 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

// 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

// 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

// 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:

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

# 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:

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

req, err := ParsePDFExportRequest(r)
if err != nil {
    HandleError(w, r, BadRequestError(err.Error()))
    return
}

DON'T: Manually parse parameters

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

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

// Validation duplicated in multiple places

3. Reuse Helper Functions

DO: Use shared data preparation

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

// 100+ lines duplicated across handlers

4. Test All Handlers

DO: Write comprehensive tests

func TestToggleLength(t *testing.T) {
    // Test toggle behavior
    // Test cookie persistence
    // Test migration from old values
}

DO: Test error cases

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

// PreferencesMiddleware reads cookies once
// Handlers get preferences from context

DON'T: Read cookies in every handler

// Cookie reading duplicated across handlers

Architectural Enhancements

Response Types

The handler layer uses standardized response types for consistent API responses:

// 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:

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:

// 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:

// 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:

// 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:

InvalidLanguageError(lang)    // Returns typed error with code
PDFGenerationError(err)       // Wraps underlying error
RateLimitError()              // Rate limit exceeded

Usage:

// 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:

# 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


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