219b83bfc0
Added comprehensive educational documentation to fill empty folders: ## Architecture Diagrams (8 files) - System architecture with layered design - Complete request/response flow diagrams - Middleware chain execution details - Handler organization structure - Data model relationships - Error handling flows - Template rendering pipeline - PDF generation process with Chromedp ## Go Patterns (9 files) - Pattern catalog and usage guide - Middleware pattern (HTTP chain composition) - Handler pattern (method-based organization) - Context pattern (request-scoped values) - Error wrapping (typed errors, chains) - Dependency injection (constructor-based) - Template pattern (rendering pipeline) - Singleton pattern (thread-safe instances) - Factory pattern (error/response constructors) ## Best Practices (2 files) - Best practices catalog and quick reference - Code organization (project structure, naming) All documentation includes: - Real examples from this project - ASCII diagrams for visualization - Best practices and anti-patterns - Testing examples - Performance considerations Documentation structure: - 20 markdown files - ~6,000+ lines of educational content - Cross-referenced between topics - Practical, project-based examples
13 KiB
13 KiB
Handler Pattern in Go
Pattern Overview
The Handler Pattern organizes HTTP endpoint logic into structured, testable components. This project uses a method-based handler approach where related endpoints are grouped as methods on a handler struct.
Pattern Structure
// Handler struct holds dependencies
type Handler struct {
tmpl *templates.Manager
db *database.DB
// other dependencies
}
// Constructor with dependency injection
func NewHandler(tmpl *templates.Manager, db *database.DB) *Handler {
return &Handler{
tmpl: tmpl,
db: db,
}
}
// HTTP handler methods
func (h *Handler) MethodName(w http.ResponseWriter, r *http.Request) {
// Handle request
}
Real Implementation from Project
CVHandler Structure
// internal/handlers/cv.go
// CVHandler handles CV-related HTTP requests
type CVHandler struct {
tmpl *templates.Manager // Template renderer
host string // Server host for absolute URLs
}
// NewCVHandler creates a new CV handler with dependencies
func NewCVHandler(tmpl *templates.Manager, host string) *CVHandler {
return &CVHandler{
tmpl: tmpl,
host: host,
}
}
Page Handlers
// internal/handlers/cv_pages.go
// Home renders the main CV page
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Get user preferences from context (set by middleware)
prefs := middleware.GetPreferences(r)
// Get language from query params, fallback to preference
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = prefs.CVLanguage
}
// Validate language
if err := validateLanguage(lang); err != nil {
h.HandleError(w, r, err)
return
}
// Prepare template data
data, err := h.prepareTemplateData(lang)
if err != nil {
h.HandleError(w, r, err)
return
}
// Render template
if err := h.tmpl.Render(w, "index.html", data); err != nil {
h.HandleError(w, r, TemplateError(err))
return
}
}
// CVContent renders just the CV content (for HTMX partial updates)
func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
prefs := middleware.GetPreferences(r)
lang := prefs.CVLanguage
data, err := h.prepareTemplateData(lang)
if err != nil {
h.HandleError(w, r, err)
return
}
if err := h.tmpl.Render(w, "partials/cv_content.html", data); err != nil {
h.HandleError(w, r, TemplateError(err))
return
}
}
HTMX Toggle Handlers
// internal/handlers/cv_htmx.go
// ToggleCVLength toggles between short and long CV formats
func (h *CVHandler) ToggleCVLength(w http.ResponseWriter, r *http.Request) {
// Get current preferences from context
prefs := middleware.GetPreferences(r)
currentLength := prefs.CVLength
// Toggle state
newLength := "long"
if currentLength == "long" {
newLength = "short"
}
// Save new preference
middleware.SetPreferenceCookie(w, "cv-length", newLength)
// Render updated content
lang := middleware.GetLanguage(r)
data, err := h.prepareTemplateData(lang)
if err != nil {
h.HandleError(w, r, err)
return
}
if err := h.tmpl.Render(w, "partials/cv_content.html", data); err != nil {
h.HandleError(w, r, TemplateError(err))
return
}
}
// ToggleCVIcons toggles icon visibility
func (h *CVHandler) ToggleCVIcons(w http.ResponseWriter, r *http.Request) {
// Similar pattern: get → toggle → save → render
// ...
}
Helper Methods
// internal/handlers/cv_helpers.go
// prepareTemplateData loads and prepares all data for template rendering
func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) {
// Load CV data
cv, err := cvmodel.LoadCV(lang)
if err != nil {
return nil, DataNotFoundError("CV", lang).WithErr(err)
}
// Load UI strings
ui, err := uimodel.LoadUI(lang)
if err != nil {
return nil, DataNotFoundError("UI", lang).WithErr(err)
}
// Calculate experience durations
for i := range cv.Experience {
cv.Experience[i].Duration = calculateDuration(
cv.Experience[i].StartDate,
cv.Experience[i].EndDate,
)
}
// Split skills into columns
skillColumns := splitSkillsIntoColumns(cv.Skills.Technical, 3)
// Build data map
return map[string]interface{}{
"CV": cv,
"UI": ui,
"SkillsColumns": skillColumns,
"PageTitle": fmt.Sprintf("%s - %s", cv.Personal.Name, cv.Personal.Title),
"CanonicalURL": h.getFullURL("/"),
}, nil
}
// getFullURL builds absolute URLs for SEO
func (h *CVHandler) getFullURL(path string) string {
return fmt.Sprintf("http://%s%s", h.host, path)
}
Handler Organization by File
Separation of Concerns
internal/handlers/
├── cv.go Constructor, shared state
├── cv_pages.go Full page renders (Home, CVContent)
├── cv_htmx.go HTMX partial updates (4 toggles)
├── cv_pdf.go PDF export endpoint
├── cv_helpers.go Shared utilities
├── types.go Request/response types
└── errors.go Error handling
This separation provides:
- Clear boundaries: Each file has a specific purpose
- Easier navigation: Find code by responsibility
- Better testing: Test files mirror source files
- Reduced conflicts: Multiple developers can work in parallel
Route Registration
// internal/routes/routes.go
func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) http.Handler {
mux := http.NewServeMux()
// Page routes
mux.HandleFunc("/", cvHandler.Home)
mux.HandleFunc("/cv", cvHandler.CVContent)
// HTMX toggle routes
mux.HandleFunc("/toggle/length", cvHandler.ToggleCVLength)
mux.HandleFunc("/toggle/icons", cvHandler.ToggleCVIcons)
mux.HandleFunc("/toggle/theme", cvHandler.ToggleCVTheme)
mux.HandleFunc("/toggle/language", cvHandler.ToggleLanguage)
// PDF export route (with additional middleware)
pdfHandler := middleware.OriginChecker(
middleware.RateLimiter(
http.HandlerFunc(cvHandler.ExportPDF),
3, // 3 requests per minute
),
)
mux.Handle("/export/pdf", pdfHandler)
// Health check
mux.HandleFunc("/health", healthHandler.Health)
// Static files
fs := http.FileServer(http.Dir("static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
// Apply global middleware
handler := middleware.Recovery(
middleware.Logger(
middleware.SecurityHeaders(
middleware.PreferencesMiddleware(mux),
),
),
)
return handler
}
Handler Benefits
1. Dependency Injection
// Dependencies are explicit and injectable
type CVHandler struct {
tmpl *templates.Manager // Can be mocked
db *database.DB // Can be mocked
cache *redis.Client // Can be mocked
}
// Easy to test with mocks
func TestHome(t *testing.T) {
mockTmpl := &MockTemplateManager{}
handler := NewCVHandler(mockTmpl, "localhost:8080")
// Test with mock
}
2. Shared Logic
// Helpers available to all handler methods
func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) {
// Reused by Home(), CVContent(), ToggleCVLength(), etc.
}
func (h *CVHandler) HandleError(w http.ResponseWriter, r *http.Request, err error) {
// Centralized error handling for all methods
}
3. Context Access
// All handler methods have access to:
// - Dependencies (h.tmpl, h.host)
// - Request (r)
// - Response (w)
func (h *CVHandler) AnyMethod(w http.ResponseWriter, r *http.Request) {
// Can access h.tmpl, h.host, etc.
}
Alternative Handler Patterns
1. Function-Based Handlers
// Simple approach for small apps
func Home(w http.ResponseWriter, r *http.Request) {
// No struct, just a function
// Dependencies passed as globals or closures
}
When to use: Very small apps, simple endpoints Drawbacks: Hard to test, shared logic difficult, no dependency injection
2. Handler with Interface
// Interface-based approach
type Handler interface {
Home(w http.ResponseWriter, r *http.Request)
Profile(w http.ResponseWriter, r *http.Request)
}
type CVHandler struct {
// ...
}
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// ...
}
When to use: Multiple implementations, complex testing Drawbacks: More boilerplate, potentially over-engineered
3. Handler with http.Handler Interface
// Implement http.Handler interface directly
type HomeHandler struct {
tmpl *templates.Manager
}
func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Handle request
}
// Register
mux.Handle("/", &HomeHandler{tmpl: tmplManager})
When to use: When you need to pass handlers around as interfaces Drawbacks: One handler per endpoint, lots of small types
Testing Handlers
Unit Test Example
// internal/handlers/cv_pages_test.go
func TestHome(t *testing.T) {
// Setup
cfg := &config.TemplateConfig{
Dir: "../../templates",
PartialsDir: "../../templates/partials",
HotReload: true,
}
tmplManager, err := templates.NewManager(cfg)
if err != nil {
t.Fatal(err)
}
handler := NewCVHandler(tmplManager, "localhost:8080")
// Create test request
req := httptest.NewRequest(http.MethodGet, "/?lang=en", nil)
w := httptest.NewRecorder()
// Execute
handler.Home(w, req)
// Verify
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, "<!DOCTYPE html>") {
t.Error("response should be HTML")
}
}
Table-Driven Tests
func TestHome(t *testing.T) {
tests := []struct {
name string
lang string
wantStatus int
wantBody string
}{
{
name: "English version",
lang: "en",
wantStatus: http.StatusOK,
wantBody: "Professional Summary",
},
{
name: "Spanish version",
lang: "es",
wantStatus: http.StatusOK,
wantBody: "Resumen Profesional",
},
{
name: "Invalid language",
lang: "xx",
wantStatus: http.StatusBadRequest,
wantBody: "INVALID_LANGUAGE",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/?lang="+tt.lang, nil)
w := httptest.NewRecorder()
handler.Home(w, req)
if w.Code != tt.wantStatus {
t.Errorf("status = %d, want %d", w.Code, tt.wantStatus)
}
if !strings.Contains(w.Body.String(), tt.wantBody) {
t.Errorf("body missing %q", tt.wantBody)
}
})
}
}
Best Practices
✅ DO
// Keep handlers focused on HTTP concerns
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Parse request
// Validate input
// Call business logic
// Render response
}
// Extract business logic to helpers
func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) {
// This can be tested independently
}
// Use dependency injection
func NewCVHandler(tmpl *templates.Manager, host string) *CVHandler {
return &CVHandler{tmpl: tmpl, host: host}
}
// Group related handlers
type CVHandler struct {
// CV-related endpoints
}
type UserHandler struct {
// User-related endpoints
}
❌ DON'T
// DON'T put business logic in handlers
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// 500 lines of business logic here...
}
// DON'T use global state
var globalTemplateManager *templates.Manager
// DON'T mix unrelated endpoints
type Handler struct {
// CV, Users, Orders, Payments all in one struct
}
// DON'T ignore errors
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
data, _ := h.prepareTemplateData(lang) // Ignoring error!
h.tmpl.Render(w, "index.html", data)
}
Handler Testing Checklist
- Test happy path
- Test invalid input
- Test missing data
- Test error handling
- Test with different preferences/context
- Test response headers
- Test response status codes
- Test response body content
Related Patterns
- Dependency Injection: Used in handler constructors
- Middleware Pattern: Wraps handlers for cross-cutting concerns
- Context Pattern: Request-scoped values in handlers
- Error Wrapping: Structured error handling in handlers