Code Refactoring Guide
Step-by-step guide for safely refactoring code without breaking functionality in the gnwebsite fullstack project.
When to Use This Skill
Use when:
- •User asks "How do I refactor this code?" or "Can I simplify this?"
- •Reducing code duplication (DRY violations)
- •Simplifying complex functions (>50 lines)
- •Extracting reusable logic to composables
- •Improving code structure or naming
- •Addressing technical debt
- •User mentions: "refactor", "clean up", "simplify", "reduce duplication"
Do NOT use for:
- •Style preferences without measurable benefit
- •Just before deadlines
- •Code without existing tests
- •Changes that alter behavior (that's feature work, not refactoring)
Decision Tree: Proposal or Direct Refactoring?
Need to refactor? │ ├─ Breaking changes? (API contracts, schemas, core patterns) │ └─ YES → Create OpenSpec proposal │ ├─ Just reorganizing code? (internal structure, no external impact) │ └─ YES → Follow safe refactoring steps │ └─ Unsure? └─ Create proposal (better safe than sorry!)
Prerequisites (CRITICAL)
NEVER refactor without tests!
# Check test coverage cd frontend && npm run test:run -- --coverage cd backend && docker exec backend pytest --cov # If coverage < 80% for code being refactored: # 1. STOP # 2. Write tests FIRST # 3. THEN refactor
Minimum requirements:
- • Unit tests exist for all functions being changed
- • Integration tests exist for workflows being changed
- • All tests currently passing ✅
Phase 1: Preparation
Step 1: Document Current Behavior
Create temporary documentation:
# Refactoring: [Component Name] ## Current Behavior - Input: X - Output: Y - Side effects: Z - Edge cases: A, B, C ## Existing Tests - test_case_1: normal flow - test_case_2: error handling - test_case_3: edge case ## Success Criteria After refactor: all tests pass, same behavior
Step 2: Create Safety Backup
# Create backup branch git checkout -b backup-before-refactor git checkout -b refactor-my-feature
Step 3: Create Refactoring Checklist
## Refactoring Checklist ### Before - [ ] All existing tests pass - [ ] Coverage documented - [ ] Behavior documented - [ ] Backup branch created ### During - [ ] ONE change at a time - [ ] Run tests after EACH change - [ ] Commit after EACH success ### After - [ ] All tests still pass - [ ] No console errors - [ ] Manual testing complete - [ ] Performance unchanged/better - [ ] Documentation updated
Phase 2: Refactoring Patterns
Pattern A: Extract Function (Reduce Complexity)
When: Function >50 lines or multiple responsibilities
Before:
async function processArticle(article: Article) {
// Validation (10 lines)
if (!article.title) throw new Error('Title required')
if (!article.content) throw new Error('Content required')
// Image processing (15 lines)
const images = []
for (const img of article.images || []) {
const url = img.image?.fileUrl || img.image?.file_url
if (url) images.push({ url, caption: img.caption })
}
// Save (20 lines)
const response = await api.create({ title, content, images })
return response
}
After:
async function processArticle(article: Article) {
validateArticle(article)
const images = processImages(article.images)
return await saveArticle(article, images)
}
function validateArticle(article: Article) {
if (!article.title) throw new Error('Title required')
if (!article.content) throw new Error('Content required')
if (article.title.length > 200) throw new Error('Title too long')
}
function processImages(images?: ArticleImage[]) {
return (images || [])
.map(img => ({ url: getImageUrl(img.image), caption: img.caption }))
.filter(img => img.url && img.url !== '/placeholder-image.png')
}
async function saveArticle(article: Article, images: ProcessedImage[]) {
return await api.create({
title: article.title,
content: article.content,
images
})
}
Steps:
- •Extract ONE function at a time
- •Run tests after each extraction
- •Commit each success
- •Add tests for new functions:
describe('validateArticle', () => {
it('should throw on missing title', () => {
expect(() => validateArticle({ content: 'test' }))
.toThrow('Title required')
})
})
describe('processImages', () => {
it('should extract URLs', () => {
const imgs = [{ image: { fileUrl: 'test.jpg' }, caption: 'Test' }]
expect(processImages(imgs)).toEqual([{ url: 'test.jpg', caption: 'Test' }])
})
it('should filter placeholders', () => {
expect(processImages([{ image: null }])).toEqual([])
})
})
Pattern B: Extract Composable (Reuse Logic)
When: Same logic duplicated across 3+ components
Before (duplicated in BlogView, ArticleView, CategoryView):
<script setup>
const items = ref([])
const loading = ref(false)
const error = ref('')
const loadItems = async () => {
loading.value = true
try {
const response = await blogService.getAll()
items.value = response.results
} catch (err) {
error.value = 'Failed to load'
} finally {
loading.value = false
}
}
onMounted(loadItems)
</script>
After:
// composables/useDataLoader.ts
export function useDataLoader<T>(
loadFn: () => Promise<{ results: T[] }>
) {
const items = ref<T[]>([])
const loading = ref(false)
const error = ref('')
const load = async () => {
loading.value = true
error.value = ''
try {
const response = await loadFn()
items.value = response.results || []
} catch (err) {
error.value = 'Failed to load items'
console.error(err)
} finally {
loading.value = false
}
}
onMounted(load)
return { items, loading, error, reload: load }
}
<!-- All components now -->
<script setup>
import { useDataLoader } from '@/composables/useDataLoader'
const { items, loading, error, reload } = useDataLoader(() =>
blogService.getAll()
)
</script>
Steps:
- •Create composable
- •Write composable tests
- •Migrate ONE component
- •Test that component
- •Commit
- •Repeat for remaining components
Pattern C: Consolidate Styles
When: Same CSS in 3+ components
Before (duplicated in 6 form components):
<style scoped>
.form-group { margin-bottom: 1.5rem; }
.form-control { width: 100%; padding: 0.75rem; }
.btn-primary { background: #007bff; color: white; }
</style>
After:
/* styles/admin-forms.css */
.form-group { margin-bottom: 1.5rem; }
.form-control { width: 100%; padding: 0.75rem; }
.btn-primary { background: #007bff; color: white; }
<!-- Components keep only unique styles -->
<style scoped>
.special-field { /* component-specific */ }
</style>
Steps:
- •Extract to shared CSS file
- •Import in main.ts/App.vue
- •Remove from ONE component
- •Visual test
- •Commit
- •Repeat for remaining
Pattern D: Replace with Utility
When: Same calculation in 5+ places
Before (in 5 files):
const imageUrl = img.image?.fileUrl || img.image?.file_url || '/placeholder-image.png'
After:
// utils/imageData.ts
export function extractImageUrl(
imageData: any,
fallback = '/placeholder-image.png'
): string {
if (!imageData) return fallback
return imageData.fileUrl || imageData.file_url || fallback
}
// All files:
const imageUrl = extractImageUrl(img.image)
Steps:
- •Create utility
- •Write comprehensive tests
- •Replace in ONE location
- •Test
- •Commit
- •Repeat for each location
Phase 3: Incremental Execution
CRITICAL WORKFLOW: One change → Test → Commit
# 1. Create working branch git checkout -b refactor-my-feature # 2. Make ONE small change # ... edit code ... # 3. Run tests npm run test:run # 4. If pass, commit git add . git commit -m "refactor: extract validateArticle function - Moved validation from processArticle - All tests passing - No behavior changes" # 5. Repeat for next change # ... edit code ... npm run test:run git commit -m "refactor: extract processImages" # Continue until complete
Testing After EVERY Change
# After each change: # 1. Unit tests npm run test:run # 2. Type check npm run type-check # 3. Pattern check npm run test:patterns # 4. Manual spot check npm run dev # Test the specific feature # If ANY fail: git reset --hard HEAD # Undo # OR fix before committing
Phase 4: Validation
Comprehensive Testing Checklist
Automated:
- • All unit tests pass
- • All integration tests pass
- • Type check passes
- • Pattern enforcement passes
- • No new linting errors
Manual:
- • Feature works exactly as before
- • No console errors
- • No visual regressions
- • Performance unchanged/better
- • Works in all browsers
Code Quality:
- • More readable
- • More testable
- • Less duplication
- • Lower complexity
- • Reasonable file sizes
Performance Verification
# Before refactor npm run build # Note: size, time # After refactor npm run build # Compare: should be similar or better
Phase 5: Documentation
Update Project Docs
If new patterns introduced:
CODEBASE_ESSENTIALS.md:
- **Image URL extraction:** Always use `extractImageUrl()` from `@/utils/imageData`. Prevents silent failures.
CODEBASE_CHANGELOG.md:
### Session: Refactor Image URL Handling (Jan 13, 2026) **Goal**: Eliminate duplicated image URL logic **Changes**: - Created `extractImageUrl()` utility - Replaced 12 instances - Added tests (8 unit, 6 integration) **Impact**: - Reduced duplication by ~80 lines - Improved testability **Validation**: - ✅ All 227 tests pass - ✅ No behavior changes - ✅ Build size unchanged **Commit**: abc123
Anti-Patterns (DON'T DO THIS)
❌ Big Bang Refactor
# WRONG: Change 50 files at once git commit -m "refactor: everything" # CORRECT: Incremental commits git commit -m "refactor: extract validation" # test git commit -m "refactor: extract image processing" # test
❌ Refactor Without Tests
// WRONG: No tests exist
function refactoredFunction() {
// Hope this works! 🤞
}
// CORRECT: Write tests first
test('refactoredFunction works', () => { ... })
function refactoredFunction() { ... }
❌ Change Behavior
// WRONG: Added new validation during refactor
function validateArticle(article: Article) {
if (!article.title) throw new Error('Title required')
if (!article.excerpt) throw new Error('Excerpt required') // NEW!
}
// CORRECT: Preserve exact behavior
function validateArticle(article: Article) {
if (!article.title) throw new Error('Title required')
// Same validation as before, just extracted
}
❌ Premature Optimization
// WRONG: No measured problem, making code complex // Replacing simple readable code with "faster" code // CORRECT: Measure first, optimize if needed // Keep code simple unless profiling shows issue
❌ Refactor Under Pressure
// WRONG: "Production deploy tomorrow, let me refactor today!" // CORRECT: Refactor when you have time to test properly
Common Scenarios
Scenario 1: Component Too Large (>300 lines)
Fix:
- •Extract child components
- •Extract composables for logic
- •Extract utilities for helpers
- •ONE responsibility per file
Scenario 2: Duplicated Code (3+ places)
Fix:
- •Identify common pattern
- •Extract to utility/composable
- •Write tests
- •Replace one by one
- •Delete duplicates
Scenario 3: Hard to Test
Fix:
- •Identify dependencies
- •Extract to parameters
- •Make functions pure
- •Write tests with mocks
Scenario 4: Unclear Naming
Fix:
- •Rename ONE identifier
- •Use IDE refactor (F2 in VS Code)
- •Run tests
- •Commit
- •Repeat
Emergency Rollback
If refactoring breaks something:
# Option 1: Revert last commit git revert HEAD # Option 2: Restore from backup git checkout backup-before-refactor git checkout -b refactor-my-feature-v2 # Option 3: Stash and investigate git stash npm run test:run # Pass now? git stash pop # Re-apply and fix # Option 4: Nuclear git reset --hard origin/development # Start over with smaller changes
Success Metrics
Refactoring succeeds when:
✅ All tests pass (no behavior changes)
✅ Code more readable (clear improvement)
✅ Complexity reduced (fewer lines, simpler logic)
✅ Duplication removed (DRY)
✅ Test coverage maintained/improved
✅ Performance unchanged/better
✅ No regressions (manual testing confirms)
Key Commands
# Before refactoring npm run test:run -- --coverage # Check coverage docker exec backend pytest --cov # Backend coverage git checkout -b backup-before-refactor # Safety backup # During refactoring (after EACH change) npm run test:run # Frontend tests npm run type-check # TypeScript npm run test:patterns # Pattern enforcement docker exec backend pytest -v # Backend tests git commit -m "refactor: [change]" # Commit success # After refactoring npm run build # Verify build npm run dev # Manual test git push origin refactor-my-feature # Push when complete
Examples
Example 1: Extract Function
# 1. Initial state: 80-line function # 2. Extract validation (commit) git commit -m "refactor: extract validateArticle" # 3. Extract image processing (commit) git commit -m "refactor: extract processImages" # 4. Simplify main function (commit) git commit -m "refactor: simplify processArticle" # Result: 3 small focused functions
Example 2: Extract Composable
# 1. Create useDataLoader composable # 2. Write tests for composable git commit -m "refactor: add useDataLoader composable" # 3. Migrate BlogView (test, commit) git commit -m "refactor: BlogView uses useDataLoader" # 4. Migrate ArticleView (test, commit) git commit -m "refactor: ArticleView uses useDataLoader" # 5. Migrate CategoryView (test, commit) git commit -m "refactor: CategoryView uses useDataLoader" # Result: Eliminated 60 lines of duplication
When to Stop
Stop refactoring when:
- •Tests start failing frequently (too aggressive)
- •Code is "good enough" (perfect is enemy of done)
- •Deadline approaching (commit what you have)
- •No measurable benefit (diminishing returns)
- •You're just tweaking style (not improving structure)
Related Resources
- •developer-checklist - Pre-commit validation
- •feature-implementation - Adding features
- •CODEBASE_ESSENTIALS.md - Current patterns
- •CODEBASE_CHANGELOG.md - Historical changes
- •.archive/guides/DRY_REFACTORING_GUIDE.md - Past refactor example