Safe code refactoring guide for gnwebsite project. Use when refactoring components, removing duplication, improving code structure, or simplifying complex functions...
Step-by-step guide for safely refactoring code without breaking functionality in the gnwebsite fullstack project.
Use when:
Do NOT use for:
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!)
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:
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
# Create backup branch
git checkout -b backup-before-refactor
git checkout -b refactor-my-feature
## 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
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:
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([])
})
})
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:
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:
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:
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
# 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
Automated:
Manual:
Code Quality:
# Before refactor
npm run build
# Note: size, time
# After refactor
npm run build
# Compare: should be similar or better
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
# 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
// WRONG: No tests exist
function refactoredFunction() {
// Hope this works! 🤞
}
// CORRECT: Write tests first
test('refactoredFunction works', () => { ... })
function refactoredFunction() { ... }
// 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
}
// 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
// WRONG: "Production deploy tomorrow, let me refactor today!"
// CORRECT: Refactor when you have time to test properly
Fix:
Fix:
Fix:
Fix:
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
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)
# 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
# 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
# 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
Stop refactoring when: