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
12 KiB
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.gowas 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:
- cv.go (29 lines) - CVHandler struct + constructor only
- cv_pages.go (290 lines) - Page rendering handlers
- cv_pdf.go (153 lines) - PDF export handler
- cv_htmx.go (218 lines) - HTMX toggle handlers
- 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 testscv_pdf_test.go- PDF generation testscv_htmx_test.go- HTMX toggle testscv_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 pageCVContent()- HTMX content swapDefaultCVShortcut()- 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
- Build: ✅
go buildsucceeds - Tests: ✅ All unit tests pass
- Server: ✅ Server starts and renders pages
- 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:
-
Package organization by feature, not by layer
- All CV handler code stays in
handlerspackage - Files split by sub-feature (pages, PDF, HTMX, helpers)
- All CV handler code stays in
-
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
-
Clear file naming
- Prefix indicates grouping:
cv_pages.go,cv_pdf.go,cv_htmx.go - Easy to find related functionality
- Prefix indicates grouping:
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
- Extract Duplicate Logic:
Home()andCVContent()have similar data preparation - could useprepareTemplateData() - Handler Tests: Add focused tests for each handler file
- Middleware Extraction: Cookie handling could become middleware
- Request/Response Types: Define structs for common request/response patterns
- Error Handling: Centralize error response formatting
Related Documentation
- Refactoring #1: CV/UI Model Separation
- Refactoring #2: Shared Utilities & Validation
- Server Design: Why Goroutines?
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