Skip to main content

Refactoring with Specs

Not all specs are features. Learn how to use LeanSpec for refactoring and architectural changes where documentation is critical.

Time: 15 minutes
Outcome: Complete a refactoring with clear documentation

Prerequisites

Try It Now

Get hands-on with a refactoring-ready project:

npx lean-spec init --example api-refactor
cd api-refactor
npm install
npm start

Then prompt your AI tool:

In your IDE with an AI coding assistant (GitHub Copilot, Claude, Cursor, etc.):

  1. Open the project in your IDE
  2. Open your AI assistant chat panel
  3. Send the prompt: "Help me refactor this app to extract a reusable API client"

Want to understand what happens under the hood? The sections below walk through each step in detail.

The Scenario

Your API client is scattered across the codebase. Every component imports Axios directly, making it hard to:

  • Add request/response interceptors
  • Switch HTTP libraries
  • Mock API calls in tests
  • Track API usage

You need to extract a centralized ApiClient class. This is a refactoring, not a feature—no user-facing changes, but the architecture improves.

Starting point:

your-project/
├── src/
│ ├── components/
│ │ ├── UserProfile.tsx # Direct axios import
│ │ └── Dashboard.tsx # Direct axios import
│ └── services/
│ ├── auth.service.ts # Direct axios import
│ └── data.service.ts # Direct axios import
└── specs/
└── (your existing specs)

Creating a Technical Spec

Ask your AI:

Create a spec for extracting a centralized API client.
Move all axios calls to a single ApiClient class with:
- Request/response interceptors
- Error handling
- Token management
- Type-safe methods

Tag it "refactoring" and "architecture".
Set priority to high.

The AI creates specs/024-centralize-api-client/README.md with:

---
status: planned
priority: high
tags: ['refactoring', 'architecture']
---

# Centralize API Client

## Overview
Extract scattered Axios calls into a single ApiClient class.

Current pain: 47 direct axios imports across codebase, no consistent
error handling, can't add auth interceptors globally, tests mock axios
instead of our API layer.

Goal: Maintainable HTTP layer with centralized error handling and
interceptors. Single import point, easy to switch HTTP libraries,
better testability.

## Design
New ApiClient class with typed get/post methods and request/response
interceptors for auth token injection and error handling.

(...diagrams or code snippets...)

Migration strategy:
1. Create ApiClient with basic methods
2. Add auth interceptor
3. Replace axios in services layer first (4 files)
4. Replace axios in components (15 files)
5. Update tests and remove axios dependency

Non-goals: Not changing API endpoints or response data structures.

## Plan
- [ ] Create src/lib/api-client.ts with typed methods
- [ ] Implement request/response interceptors
- [ ] Migrate services layer
- [ ] Migrate components layer
- [ ] Update test mocks
- [ ] Remove direct axios imports

## Test
- [ ] All existing tests pass
- [ ] ApiClient tests cover error handling
- [ ] Auth tokens properly injected in requests

What's different here? This is a technical spec:

  • Focuses on architecture, not user features
  • Includes migration strategy (order matters)
  • Explicit non-goals prevent scope creep
  • Detailed plan because refactoring is risky

Implementation

Ask your AI:

Implement spec 024. Take it step by step:
1. Create ApiClient first
2. Show me the implementation before migrating
3. Then migrate one service as a test
4. Wait for my approval before continuing

The AI proceeds incrementally:

Step 1: Creates src/lib/api-client.ts

export class ApiClient {
private client: AxiosInstance;

constructor() {
this.client = axios.create({ baseURL: process.env.API_BASE_URL });
// Auth interceptor adds token to requests
}

async get<T>(path: string): Promise<T> { /* ... */ }
async post<T>(path: string, data: any): Promise<T> { /* ... */ }
}

Step 2: Migrates one service as proof

// src/services/auth.service.ts
- import axios from 'axios';
+ import { apiClient } from '@/lib/api-client';

- const { data } = await axios.post('/auth/login', credentials);
+ const data = await apiClient.post('/auth/login', credentials);

You review, test, and approve: "Looks good. Continue with remaining services and components."

Step 3: AI migrates remaining files, updates tests, removes axios imports

Review and Complete

Run the test suite:

npm test

All tests pass? Verify in dev:

npm run dev
# Click around, check network tab

Everything works. Complete the spec:

lean-spec update 024 --status complete

The spec now documents why you made this change and how it was done. Future developers can read it to understand the architecture decision.

What Just Happened

You documented and executed a complex refactoring:

Without LeanSpec:

  • Refactoring rationale lost in PR description
  • Implementation details in git history (hard to find)
  • Future devs ask "Why did we do this?"
  • Scope creep: "While we're refactoring, let's also..."

With LeanSpec:

  • Clear rationale captured before coding
  • Migration strategy documented
  • AI follows the plan step-by-step
  • Non-goals prevent feature creep
  • Completed spec = permanent record

The spec becomes architectural documentation that explains past decisions to future maintainers.

When to Spec a Refactoring

Write a spec when:

  • Refactoring affects >5 files
  • Architecture decision needs explanation
  • Migration order matters
  • Risk of breaking existing behavior

Skip the spec when:

  • Renaming variables/files
  • Extracting a single function
  • Fixing code style
  • Obvious improvements

Refactoring spec checklist:

  • "Why this matters" section explains pain
  • Design shows before/after structure
  • Migration strategy is explicit
  • Non-goals prevent scope creep
  • Test criteria ensure no regression

Key Patterns

Technical specs include:

  • Rationale: Why we're doing this (pain points)
  • Design: Architecture changes (diagrams help)
  • Migration: Order of changes (critical for refactoring)
  • Non-goals: What we're NOT doing (scope control)
  • Testing: How to verify nothing broke

AI assistance works best with:

  • Step-by-step instructions ("do X, wait for approval")
  • Clear migration order
  • Test-after-each-phase approach
  • Explicit file paths

Next Steps

Try refactoring something in your codebase:

  • Pick a scattered concern (logging, errors, config)
  • Create a spec with migration strategy
  • Let AI implement step-by-step
  • Complete and document the decision

Learn more:

Advanced:

Questions? See FAQ or start a discussion.