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
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
- Handler Architecture
- Request/Response Types
- Middleware Pattern
- Testing Strategy
- Data Flow
- Best Practices
- 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:
- 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
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 responseNewErrorResponse(code, message)- Create error responseErrorResponseWithField(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 handlerBenchmarkCVContent- Content renderingBenchmarkToggleLength- Toggle handlersBenchmarkParsePDFExportRequest- Request parsingBenchmarkPrepareTemplateData- Data preparationBenchmarkParallelHome- Parallel load test- Response creation benchmarks
Middleware (12 benchmarks):
BenchmarkPreferencesMiddleware- Middleware performanceBenchmarkGetPreferences- Context retrievalBenchmarkGetLanguage- Helper functionsBenchmarkIsLongCV- Boolean helpersBenchmarkParallelPreferencesMiddleware- 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
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: 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