Create public-facing documentation explaining backend architecture: New Documentation: - doc/14-BACKEND-HANDLERS.md (900+ lines) * Handler architecture and file organization * Request/response type system with examples * Middleware pattern and preferences handling * Comprehensive testing strategy * Data flow diagrams and best practices * Code examples for all major patterns Updated: - doc/README.md * Add Backend Handlers to technical implementation section * Update total active docs count (13 → 14) * Add quick navigation links Content Coverage: - Handler responsibilities (pages, PDF, HTMX) - Type-safe request handling with validation - Middleware architecture and context usage - Test coverage across all handler types - Request processing flow diagrams - Best practices with do/don't examples Audience: - Backend developers - API consumers - New contributors - Technical documentation readers Complements: - Educational docs in _go-learning/refactorings/ - Internal architecture documentation - API reference guide
14 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
- Handler Architecture
- Request/Response Types
- Middleware Pattern
- Testing Strategy
- Data Flow
- 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
// 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:
- User clicks toggle button
- HTMX sends POST request
- Handler updates cookie
- Handler returns HTML fragment with out-of-band swaps
- 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
Related Documentation
- Architecture Overview - System architecture patterns
- API Reference - Complete API documentation
- Security - Security implementation details
- PDF Export - PDF generation details
- Testing Guide - Detailed refactoring documentation
Changelog
- Nov 20, 2024: Initial documentation covering handler refactoring, type safety, middleware pattern, and testing strategy