Files
cv-site/doc/_go-learning/refactorings/003-handler-split.md
T
juanatsap d95c62bad4 refactor: remove outdated server design documentation
Remove 557-line server-design.md from _go-learning/architecture - content is now covered in updated architecture documentation with real implementation examples and test coverage.
2025-12-02 20:25:05 +00:00

12 KiB
Raw Blame History

Refactoring #3: Handler Split - From Monolith to Focused Files

Date: 2024-11-20 Type: Code Organization, Maintainability

Problem Statement

After implementing shared utilities and validation (Refactoring #2), the handler file remained problematic:

  • Single Monolithic File: internal/handlers/cv.go was 1,001 lines
  • Mixed Concerns: Page rendering, PDF export, HTMX toggles, and helpers all in one file
  • Difficult Navigation: Finding specific functionality required scrolling through hundreds of lines
  • Poor Separation: No clear boundaries between different types of handlers

Solution

Split the monolithic handler into focused files by responsibility:

  1. cv.go (29 lines) - CVHandler struct + constructor only
  2. cv_pages.go (290 lines) - Page rendering handlers
  3. cv_pdf.go (153 lines) - PDF export handler
  4. cv_htmx.go (218 lines) - HTMX toggle handlers
  5. cv_helpers.go (385 lines) - Helper functions

Architecture

Before (Monolithic)

internal/handlers/cv.go (1,001 lines)
├── CVHandler struct
├── NewCVHandler()
├── Home()                      (page handler)
├── CVContent()                 (page handler)
├── DefaultCVShortcut()         (page handler)
├── ExportPDF()                 (PDF handler)
├── ToggleLength()              (HTMX handler)
├── ToggleIcons()               (HTMX handler)
├── SwitchLanguage()            (HTMX handler)
├── ToggleTheme()               (HTMX handler)
├── splitSkills()               (helper)
├── calculateYearsOfExperience() (helper)
├── calculateDuration()         (helper)
├── processProjectDates()       (helper)
├── findProjectRoot()           (helper)
├── validateRepoPath()          (helper)
├── getGitRepoFirstCommitDate() (helper)
├── prepareTemplateData()       (helper)
├── getPreferenceCookie()       (helper)
└── setPreferenceCookie()       (helper)

After (Focused Files)

internal/handlers/
├── cv.go (29 lines)
│   ├── CVHandler struct
│   └── NewCVHandler()
│
├── cv_pages.go (290 lines)
│   ├── Home()              - Full CV page
│   ├── CVContent()         - HTMX content swap
│   └── DefaultCVShortcut() - Shortcut PDF URLs
│
├── cv_pdf.go (153 lines)
│   └── ExportPDF()         - PDF generation with options
│
├── cv_htmx.go (218 lines)
│   ├── ToggleLength()      - Short/long toggle
│   ├── ToggleIcons()       - Show/hide icons
│   ├── SwitchLanguage()    - EN/ES switching
│   └── ToggleTheme()       - Default/clean theme
│
└── cv_helpers.go (385 lines)
    ├── Skills helpers:
    │   └── splitSkills()
    ├── Date/Duration helpers:
    │   ├── calculateYearsOfExperience()
    │   ├── calculateDuration()
    │   └── processProjectDates()
    ├── Git helpers:
    │   ├── findProjectRoot()
    │   ├── validateRepoPath()
    │   └── getGitRepoFirstCommitDate()
    ├── Template helpers:
    │   └── prepareTemplateData()
    └── Cookie helpers:
        ├── getPreferenceCookie()
        └── setPreferenceCookie()

Benefits

1. Single Responsibility Principle (SRP)

Each file now has ONE clear purpose:

cv.go - Defines the handler structure

// CVHandler handles CV-related requests
// Methods are split across multiple files for better organization:
// - cv_pages.go: Page rendering (Home, CVContent, DefaultCVShortcut)
// - cv_pdf.go: PDF export (ExportPDF)
// - cv_htmx.go: HTMX toggles (ToggleLength, ToggleIcons, SwitchLanguage, ToggleTheme)
// - cv_helpers.go: Helper functions (skills, dates, git, templates, cookies)
type CVHandler struct {
	templates    *templates.Manager
	pdfGenerator *pdf.Generator
	serverAddr   string
}

2. Improved Discoverability

Easy to find functionality:

  • Need to modify page rendering? → cv_pages.go
  • PDF generation issue? → cv_pdf.go
  • HTMX toggle not working? → cv_htmx.go
  • Helper function bug? → cv_helpers.go

3. Reduced Cognitive Load

Before: Navigate 1,001 lines to understand one feature After: Open the relevant ~150-400 line file

4. Better Code Organization

cv_helpers.go groups helpers by category with clear section markers:

// ==============================================================================
// SKILLS HELPERS
// ==============================================================================

// splitSkills splits skill categories between left and right sidebars
func splitSkills(skills []cvmodel.SkillCategory) (left, right []cvmodel.SkillCategory) {
    // ...
}

// ==============================================================================
// DATE/DURATION HELPERS
// ==============================================================================

// calculateYearsOfExperience calculates years since April 1, 2005
func calculateYearsOfExperience() int {
    // ...
}

5. Parallel Development

Multiple developers can now work on different handler concerns without conflicts:

  • Developer A: Adds new HTMX toggle → edits cv_htmx.go
  • Developer B: Modifies PDF export → edits cv_pdf.go
  • Developer C: Adds page handler → edits cv_pages.go

No merge conflicts!

6. Testability

Each file can have focused tests:

  • cv_pages_test.go - Page rendering tests
  • cv_pdf_test.go - PDF generation tests
  • cv_htmx_test.go - HTMX toggle tests
  • cv_helpers_test.go - Helper function tests

7. Documentation Clarity

Each file's purpose is immediately clear from its name and can have targeted documentation.

Implementation Details

Why These Groupings?

cv_pages.go - All handlers that render full pages or page sections

  • Home() - Complete HTML page
  • CVContent() - HTMX content swap
  • DefaultCVShortcut() - Special PDF shortcut URLs

cv_pdf.go - PDF generation is complex enough to warrant its own file

  • Handles multiple query parameters (lang, length, icons, version)
  • Manages PDF generation with chromedp
  • Complex filename generation logic

cv_htmx.go - All HTMX interactivity handlers

  • Similar patterns (toggle states, cookies, out-of-band swaps)
  • All follow same structure: read state → toggle → save → render

cv_helpers.go - All supporting functions

  • Organized by category with section markers
  • Pure functions (no HTTP request/response handling)
  • Reusable across handlers

Go Package Benefits

All files are in the same package (package handlers), so:

  • Methods can be split across files (Go allows this!)
  • Helper functions accessible without imports
  • No circular dependency issues
  • Same namespace, better organization

Code Metrics

File Sizes

File Lines Purpose Complexity
cv.go 29 Struct + constructor Very Low
cv_pages.go 290 Page rendering Medium
cv_pdf.go 153 PDF export Medium
cv_htmx.go 218 HTMX toggles Low
cv_helpers.go 385 Helper functions Low-Medium
Total 1,075 Average

Reduction Achievement

  • Original: 1 file × 1,001 lines = 1,001 lines
  • New: 5 files × 215 lines avg = 1,075 lines
  • Net Change: +74 lines (+7.4%)

The slight increase is due to:

  • Comments documenting each file's purpose
  • Section markers in cv_helpers.go for better organization
  • More descriptive comments at file level

Trade-off: +74 lines for dramatically improved maintainability and organization.

Maintainability Index

Before:

  • 1,001 lines to search through
  • 19 functions mixed together
  • No clear organization

After:

  • 29-385 lines per file
  • 3-9 functions per file (focused)
  • Clear organization by responsibility

Testing

All Tests Pass

$ go test ./...
ok      github.com/juanatsap/cv-site/internal/fileutil  0.432s
ok      github.com/juanatsap/cv-site/internal/handlers  0.789s
ok      github.com/juanatsap/cv-site/internal/lang      0.326s
ok      github.com/juanatsap/cv-site/internal/models/cv 0.463s
ok      github.com/juanatsap/cv-site/internal/models/ui 0.315s

Verification

  1. Build: go build succeeds
  2. Tests: All unit tests pass
  3. Server: Server starts and renders pages
  4. Endpoints: All HTTP endpoints functional

Why This Approach?

Alternative Considered: Separate Packages

Could we split into separate packages?

internal/
├── handlers/pages/
├── handlers/pdf/
├── handlers/htmx/
└── handlers/helpers/

Why NOT:

  • Creates circular dependencies (pages need helpers, helpers need CVHandler)
  • More complex imports
  • Breaks Go's "methods on types" pattern (can't split CVHandler methods across packages)

Why Single Package:

  • Methods can be defined in any file
  • Helpers accessible without imports
  • Single namespace, no confusion
  • Go's design encourages this pattern

Go Best Practices

This approach follows Go best practices:

  1. Package organization by feature, not by layer

    • All CV handler code stays in handlers package
    • Files split by sub-feature (pages, PDF, HTMX, helpers)
  2. Methods split across files

    • Go allows defining methods on a type in any file in the same package
    • CVHandler methods spread across multiple files naturally
  3. Clear file naming

    • Prefix indicates grouping: cv_pages.go, cv_pdf.go, cv_htmx.go
    • Easy to find related functionality

Interview Talking Points

1. Code Organization

"I refactored a 1,001-line monolithic handler into 5 focused files (29-385 lines each), improving discoverability and maintainability while following Go's single-package-multiple-files pattern."

2. Single Responsibility Principle

"Each file now has one clear purpose: cv_pages handles page rendering, cv_pdf manages PDF export, cv_htmx handles interactivity, and cv_helpers provides reusable functions."

3. Maintainability Over Brevity

"I accepted a 7.4% line increase to gain dramatically improved organization. The trade-off of 74 extra lines for better maintainability was worth it."

4. Go Package Patterns

"I kept all files in one package to avoid circular dependencies and leverage Go's ability to split methods across files, rather than forcing artificial package boundaries."

5. Parallel Development

"The split enables multiple developers to work on different handler concerns without conflicts, improving team velocity."

6. Progressive Refactoring

"This is refactoring #3 in a series: #1 separated domain models, #2 added shared utilities and validation, #3 organized handlers. Each step builds on the previous, improving the codebase incrementally."

Future Improvements

  1. Extract Duplicate Logic: Home() and CVContent() have similar data preparation - could use prepareTemplateData()
  2. Handler Tests: Add focused tests for each handler file
  3. Middleware Extraction: Cookie handling could become middleware
  4. Request/Response Types: Define structs for common request/response patterns
  5. Error Handling: Centralize error response formatting

Commit Message

refactor: Split monolithic handler into focused files

Split internal/handlers/cv.go (1,001 lines) into 5 focused files:

Structure:
- cv.go (29 lines) - CVHandler struct + constructor
- cv_pages.go (290 lines) - Page handlers (Home, CVContent, DefaultCVShortcut)
- cv_pdf.go (153 lines) - PDF export handler (ExportPDF)
- cv_htmx.go (218 lines) - HTMX toggle handlers (Length, Icons, Language, Theme)
- cv_helpers.go (385 lines) - Helper functions (skills, dates, git, templates, cookies)

Benefits:
- Single Responsibility: Each file has one clear purpose
- Improved Discoverability: Easy to find specific functionality
- Reduced Cognitive Load: 200-400 lines per file vs 1,001
- Parallel Development: No conflicts when editing different concerns
- Better Organization: Clear section markers and grouping
- Maintainability: Trade +74 lines (+7.4%) for better organization

Testing:
- All Go tests pass (fileutil, handlers, lang, cv, ui)
- Server builds and runs correctly
- All HTTP endpoints functional
- No breaking changes

Documentation:
- Create _go-learning/refactorings/003-handler-split.md
- Document architecture, benefits, and trade-offs
- Explain WHY single package vs separate packages