Files
cv-site/doc/_go-learning/patterns/02-handler-pattern.md
T

529 lines
13 KiB
Markdown
Raw Normal View History

# 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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:
1. **Clear boundaries**: Each file has a specific purpose
2. **Easier navigation**: Find code by responsibility
3. **Better testing**: Test files mirror source files
4. **Reduced conflicts**: Multiple developers can work in parallel
## Route Registration
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```go
// 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
```go
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
```go
// 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
```go
// 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
## Further Reading
- [HTTP Handler Pattern](https://www.alexedwards.net/blog/a-recap-of-request-handling)
- [Structuring Go Applications](https://www.gobeyond.dev/standard-package-layout/)
- [Dependency Injection](./05-dependency-injection.md)