docs: Add comprehensive backend handler documentation
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
This commit is contained in:
@@ -0,0 +1,566 @@
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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**: Initial documentation covering handler refactoring, type safety, middleware pattern, and testing strategy
|
||||
+3
-1
@@ -18,6 +18,7 @@
|
||||
- [5. Zoom Implementation](5-ZOOM-IMPLEMENTATION.md) - Custom zoom feature technical details
|
||||
- [12. CSS Architecture](12-CSS-ARCHITECTURE.md) - Modular CSS structure and ITCSS organization ⭐
|
||||
- [13. Toast Notifications](13-TOAST-NOTIFICATIONS.md) - Toast notification system for PDF downloads and user feedback
|
||||
- [14. Backend Handlers](14-BACKEND-HANDLERS.md) - Handler architecture, type safety, middleware, and testing ⭐
|
||||
|
||||
**Deployment & Operations**
|
||||
- [8. Deployment Guide](8-DEPLOYMENT.md) - Production deployment instructions
|
||||
@@ -46,6 +47,7 @@
|
||||
| 5 | [ZOOM_IMPLEMENTATION.md](5-ZOOM-IMPLEMENTATION.md) | Zoom feature implementation details | Feature developers |
|
||||
| 12 | [CSS-ARCHITECTURE.md](12-CSS-ARCHITECTURE.md) | Modular CSS structure, ITCSS layers, HTMX integration | Frontend developers, designers |
|
||||
| 13 | [TOAST-NOTIFICATIONS.md](13-TOAST-NOTIFICATIONS.md) | Toast notification system, PDF download feedback, user notifications | Frontend developers, UX designers |
|
||||
| 14 | [BACKEND-HANDLERS.md](14-BACKEND-HANDLERS.md) | Handler architecture, type safety, middleware pattern, testing strategy | Backend developers |
|
||||
|
||||
### User & Operations Documentation
|
||||
|
||||
@@ -141,4 +143,4 @@ All documentation in this project follows these standards:
|
||||
|
||||
**Last Updated**: 2025-11-20
|
||||
**Documentation Status**: ✅ Clean, organized, zero redundancy
|
||||
**Total Active Docs**: 13 core documents + archive
|
||||
**Total Active Docs**: 14 core documents + archive
|
||||
|
||||
Reference in New Issue
Block a user