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

733 lines
19 KiB
Markdown

# 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