2025-11-20 17:35:58 +00:00
# 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 )
2025-11-20 18:24:41 +00:00
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 )
2025-11-20 17:35:58 +00:00
---
## 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
```
---
2025-11-20 18:24:41 +00:00
## 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
```
---
2025-11-20 17:35:58 +00:00
## 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
2025-11-20 18:24:41 +00:00
- **Nov 20, 2024**: Added architectural enhancements section (response types, validation tags, context helpers, typed errors, benchmarks)
2025-11-20 17:35:58 +00:00
- **Nov 20, 2024**: Initial documentation covering handler refactoring, type safety, middleware pattern, and testing strategy