Claude Code Mastery

Refactoring mit Claude Code:
Legacy-Code modernisieren 2026

📅 6. Mai 2026 ⏱ 11 min Lesezeit ✍️ SpockyMagicAI Redaktion
Thema: Legacy-Code modernisieren
Stack: JS → TS, React, SQL, ORM
Tool: Claude Code CLI
Level: Intermediate–Advanced

Legacy-Codebasen sind der Alltag der meisten Entwicklerteams. Callback-Pyramiden aus dem Jahr 2015, ungetyptes JavaScript mit 10.000 Zeilen, Class-Components die noch vor React Hooks entstanden sind — wer kennt das nicht? Claude Code verändert grundlegend, wie Teams solche Modernisierungsprojekte angehen können: nicht als großes Big-Bang-Rewrite, sondern als sicherer, schrittweiser Prozess mit KI als Co-Pilot.

Dieser Leitfaden zeigt konkrete Refactoring-Strategien für die häufigsten Legacy-Muster: JavaScript → TypeScript, Callback-Hell → async/await, React-Klassen → Hooks, rohe SQL-Abfragen → ORM und Monolith → modulare Architektur. Für jedes Muster gibt es einen Claude Code Prompt sowie realistische Before/After-Beispiele.

📋 Inhalt

  1. Refactoring-Strategie: Sicher modernisieren ohne Regressionen
  2. JavaScript → TypeScript: Typen Schritt für Schritt einführen
  3. Callbacks → async/await: Callback-Hell flach machen
  4. Klassen → React Hooks: Class Components modernisieren
  5. Rohe SQL → ORM: Datenbanklogik sicher migrieren
  6. Monolith → Module: Große Dateien aufteilen

1. Refactoring-Strategie: Sicher modernisieren ohne Regressionen

Das größte Risiko beim Refactoring ist nicht mangelndes Wissen — es sind fehlende Sicherheitsnetze. Ohne Tests, ohne strukturiertes Vorgehen und ohne klare Rollback-Strategie endet jede Modernisierungsinitiative in einem "es hat vorher irgendwie funktioniert"-Fiasko.

Strategie

Das Refactoring-Sicherheitsnetz

"Analysiere diese Datei und erstelle Charakterisierungstests die das aktuelle Verhalten aller exportierten Funktionen dokumentieren. Fokus auf Edge Cases und Fehlerfälle. Nutze Jest/Vitest Syntax. Ziel: ein Sicherheitsnetz, das Regressionen sofort sichtbar macht."

Claude Code ist dabei besonders wertvoll in der Analysephase: Es kann eine Legacy-Datei lesen, alle Abhängigkeiten und Seiteneffekte kartieren und dann gezielt Tests für die riskantesten Stellen generieren — ohne dass der Entwickler jede Code-Zeile im Kopf haben muss.

Der Refactoring-Workflow mit Claude Code

Profi-Tipp:

Starte jede Refactoring-Session mit: "Lies die Datei X und erstelle einen Refactoring-Plan in 5 Schritten. Gib nach jedem Schritt an, welche Tests ich ausführen soll." So bleibt Claude Code fokussiert und du behältst den Überblick.

2. JavaScript → TypeScript: Typen Schritt für Schritt einführen

Die Migration von JavaScript zu TypeScript ist eine der häufigsten Modernisierungsaufgaben. Das Ziel ist nicht, alles auf einmal mit strict: true zu erschlagen — sondern inkrementell vorzugehen und dabei maximalen Wert mit minimalem Risiko zu erzielen.

TypeScript

JS → TS Migrations-Strategie

"Migriere diese JavaScript-Datei zu TypeScript. Inferiere Typen aus der Verwendung wo möglich, nutze JSDoc-Annotationen als Hinweise. Vermeide any vollständig. Erstelle separate Interface-Definitionen für alle Objekte die mehr als 3 Properties haben. Erkläre deine Typ-Entscheidungen in Inline-Kommentaren."

❌ Vorher: JavaScript
// user-service.js function getUser(id) { return db.query( 'SELECT * FROM users WHERE id = ?', [id] ); } function updateUser(id, data) { if (!data.name || !data.email) { throw new Error('Invalid data'); } return db.query( 'UPDATE users SET ...', [data.name, data.email, id] ); } async function createUser(data) { const user = await db.insert('users', data); return user; }
✅ Nachher: TypeScript
// user-service.ts interface User { id: number; name: string; email: string; createdAt: Date; } type CreateUserInput = Pick<User, 'name' | 'email'>; type UpdateUserInput = Partial<CreateUserInput>; async function getUser(id: number): Promise<User | null> { return db.query<User>( 'SELECT * FROM users WHERE id = ?', [id] ); } async function updateUser( id: number, data: UpdateUserInput ): Promise<User> { if (!data.name || !data.email) { throw new Error('Invalid data'); } return db.query<User>( 'UPDATE users SET ...', [data.name, data.email, id] ); } async function createUser( data: CreateUserInput ): Promise<User> { return db.insert<User>('users', data); }

JSDoc → TypeScript: Automatische Inferenz nutzen

Viele ältere Codebasen haben JSDoc-Kommentare als informelle Typdokumentation. Claude Code kann diese als Ausgangspunkt nutzen und echte TypeScript-Typen daraus ableiten.

❌ Vorher: JSDoc
/** * @param {Object} options * @param {string} options.apiKey * @param {number} [options.timeout] * @param {boolean} [options.retry] * @returns {Promise<Object>} */ async function fetchData(options) { const { apiKey, timeout = 5000, retry = false } = options; // implementation }
✅ Nachher: Echte Typen
interface FetchOptions { apiKey: string; timeout?: number; retry?: boolean; } interface ApiResponse<T = unknown> { data: T; status: number; headers: Record<string, string>; } async function fetchData<T = unknown>( options: FetchOptions ): Promise<ApiResponse<T>> { const { apiKey, timeout = 5000, retry = false } = options; // implementation }
⚠️ Häufiger Fehler:

Niemals as any als permanente Lösung akzeptieren. Claude Code kann angewiesen werden: "Markiere jedes any mit einem TODO-Kommentar und schlage den korrekten Typ vor." So bleiben keine stillen Typlöcher im Code.

3. Callbacks → async/await: Callback-Hell flach machen

Callback-Pyramiden sind eines der sichtbarsten Zeichen für JavaScript-Altcode. Drei oder vier Ebenen tief verschachtelter Callbacks machen Code schwer lesbar, schwer testbar und fehleranfällig. async/await ist die moderne Antwort — und Claude Code kann Callback-Ketten systematisch transformieren.

Async

Callback → Promise → async/await

"Transformiere diese Callback-basierten Funktionen zu async/await. Identifiziere welche Operationen parallel laufen können (Promise.all) und welche sequenziell sein müssen. Füge typsicheres Error-Handling mit try/catch hinzu. Behalte die gleiche Schnittstelle nach außen."

❌ Vorher: Callback-Hell
getUserById(userId, function(err, user) { if (err) { return handleError(err); } getOrdersForUser(user.id, function(err, orders) { if (err) { return handleError(err); } getProductDetails(orders, function(err, products) { if (err) { return handleError(err); } renderDashboard({ user, orders, products }); }); }); });
✅ Nachher: async/await
async function loadDashboard( userId: string ): Promise<void> { try { const user = await getUserById(userId); // Parallel laden wo möglich const [orders, preferences] = await Promise.all([ getOrdersForUser(user.id), getUserPreferences(user.id), ]); const products = await getProductDetails(orders); renderDashboard({ user, orders, products, preferences }); } catch (error) { handleError(error as Error); } }

Promise.allSettled für fehlertolerante Parallelität

❌ Vorher: Sequenziell
async function loadWidgets() { const news = await fetchNews(); const weather = await fetchWeather(); const stocks = await fetchStocks(); // 3× sequenziell = langsamste Route! return { news, weather, stocks }; }
✅ Nachher: Parallel + Fehlerresistent
async function loadWidgets() { const results = await Promise.allSettled([ fetchNews(), fetchWeather(), fetchStocks(), ]); return { news: results[0].status === 'fulfilled' ? results[0].value : null, weather: results[1].status === 'fulfilled' ? results[1].value : null, stocks: results[2].status === 'fulfilled' ? results[2].value : null, }; }
Claude Code Prompt für AbortController:

"Füge AbortController-Support zu dieser async Funktion hinzu. Der Controller soll automatisch nach [X] Sekunden abbrechen und bei Component-Unmount in React gecancelt werden können."

4. Klassen → React Hooks: Class Components modernisieren

React Class Components sind funktional noch valide — aber im modernen React-Ökosystem ein Auslaufmodell. Hooks sind leichter testbar, besser composebar und brauchen weniger Boilerplate. Claude Code kann Class Components systematisch transformieren und dabei alle Lifecycle-Methoden korrekt abbilden.

React Hooks

Class → Function Component Mapping

Klassen-Muster Hook-Äquivalent
this.stateuseState()
componentDidMountuseEffect([], [])
componentDidUpdateuseEffect([deps])
componentWillUnmountuseEffect Cleanup-Funktion
shouldComponentUpdateReact.memo()
HOC-PatternCustom Hook
this.forceUpdate()useReducer

"Konvertiere diese React Class Component zu einer Function Component mit Hooks. Bilde alle Lifecycle-Methoden korrekt auf useEffect ab. Extrahiere Geschäftslogik in einen Custom Hook use[ComponentName]. Behalte Props-Interface identisch. Füge React.memo hinzu wo sinnvoll."

❌ Vorher: Class Component
class UserProfile extends Component { state = { user: null, loading: true, error: null }; componentDidMount() { this.fetchUser(); } componentDidUpdate(prevProps) { if (prevProps.userId !== this.props.userId) { this.fetchUser(); } } fetchUser = async () => { try { const user = await getUser(this.props.userId); this.setState({ user, loading: false }); } catch (error) { this.setState({ error, loading: false }); } }; render() { const { user, loading, error } = this.state; if (loading) return <Spinner />; if (error) return <ErrorMsg error={error} />; return <UserCard user={user} />; } }
✅ Nachher: Function + Custom Hook
// useUserProfile.ts — Custom Hook function useUserProfile(userId: string) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); useEffect(() => { let cancelled = false; setLoading(true); getUser(userId) .then(u => { if (!cancelled) setUser(u); }) .catch(e => { if (!cancelled) setError(e); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [userId]); return { user, loading, error }; } // UserProfile.tsx — Schlanke Component const UserProfile = React.memo(({ userId }: { userId: string }) => { const { user, loading, error } = useUserProfile(userId); if (loading) return <Spinner />; if (error) return <ErrorMsg error={error} />; return <UserCard user={user!} />; });

HOCs → Custom Hooks

Higher Order Components (HOCs) waren das Composability-Muster vor Hooks. Custom Hooks sind lesbarer, vermeiden Props-Drilling und können ohne Wrapper-Hölle kombiniert werden.

❌ Vorher: HOC-Pattern
function withAuth(WrappedComponent) { return function(props) { const isAuth = checkAuth(); if (!isAuth) return <Redirect to="/login" />; return <WrappedComponent {...props} />; }; } export default withAuth(withTheme(withLocale(MyPage))));
✅ Nachher: Custom Hooks
function useAuth() { const [isAuth, setIsAuth] = useState(checkAuth()); return { isAuth }; } function MyPage() { const { isAuth } = useAuth(); const { theme } = useTheme(); const { locale } = useLocale(); if (!isAuth) return <Navigate to="/login" />; // Kein HOC-Wrapper-Chaos mehr return <div>...</div>; }

5. Rohe SQL → ORM: Datenbanklogik sicher migrieren

Raw-SQL-Strings im Anwendungscode sind ein Wartungsproblem: keine Typsicherheit, manuelle Sanitisierung, keine einfache Migration bei Schema-Änderungen. Moderne ORMs wie Prisma oder Drizzle ORM lösen diese Probleme — und Claude Code kann die Migration systematisch durchführen.

ORM

ORM-Migrations-Checkliste

"Analysiere alle SQL-Queries in dieser Datei. Erstelle ein vollständiges Prisma-Schema für die verwendeten Tabellen. Konvertiere dann jede Query in die entsprechende Prisma-Client-API. Identifiziere N+1-Probleme und löse sie mit include oder select. Wickle zusammengehörende Mutations in Transaktionen ein."

❌ Vorher: Raw SQL
// Untypisiertes Raw SQL async function getOrdersWithItems(userId) { const orders = await db.query( `SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC`, [userId] ); // N+1 Problem! for (const order of orders.rows) { const items = await db.query( `SELECT * FROM order_items WHERE order_id = $1`, [order.id] ); order.items = items.rows; } return orders.rows; }
✅ Nachher: Prisma ORM
// Prisma Schema (schema.prisma) // model Order { ... items OrderItem[] } async function getOrdersWithItems( userId: string ) { // Typsicher, kein N+1 return prisma.order.findMany({ where: { userId }, orderBy: { createdAt: 'desc' }, include: { items: { include: { product: true }, }, }, }); } // Rückgabetyp automatisch inferiert: // Order & { items: (OrderItem & { product: Product })[] }

Transaktionen mit Prisma

❌ Vorher: Manuelle Transaktion
async function placeOrder(cart) { await db.query('BEGIN'); try { const order = await db.query( 'INSERT INTO orders ...', [...] ); await db.query( 'INSERT INTO order_items ...', [...] ); await db.query( 'UPDATE inventory ...', [...] ); await db.query('COMMIT'); return order; } catch (e) { await db.query('ROLLBACK'); throw e; } }
✅ Nachher: Prisma Transaktion
async function placeOrder( cart: CartItem[] ): Promise<Order> { return prisma.$transaction(async (tx) => { const order = await tx.order.create({ data: { /* ... */ }, }); await tx.orderItem.createMany({ data: cart.map(item => ({ orderId: order.id, ...item })), }); await tx.inventory.updateMany({ where: { productId: { in: cart.map(i => i.productId) } }, data: { /* decrement stock */ }, }); return order; }); }
Drizzle ORM Alternative:

Drizzle ist SQL-first und erzeugt typsichere Queries die nah an raw SQL bleiben. Claude Code Prompt: "Erstelle ein Drizzle-Schema für diese SQL-Tabellen und konvertiere alle Queries. Behalte den SQL-ähnlichen Stil von Drizzle."

6. Monolith → Module: Große Dateien aufteilen

Eine 3.000-Zeilen-Datei die alles macht — API-Calls, Formatierung, Validierung, State-Management — ist ein klassisches Zeichen für organisch gewachsenen Code. Das Aufteilen in Module verbessert Testbarkeit, Wiederverwendbarkeit und die Zusammenarbeit im Team dramatisch.

Module

Modularisierungs-Strategie

"Analysiere diese 2000-Zeilen-Datei. Erstelle eine Karte aller Verantwortlichkeiten und internen Abhängigkeiten. Schlage eine Verzeichnisstruktur vor die Single-Responsibility und Feature-Kohäsion maximiert. Erstelle dann für jedes Modul eine separate Datei mit einem zentralen index.ts der die öffentliche API re-exportiert."

❌ Vorher: Monolith
// utils.ts — 2000 Zeilen export function formatCurrency(n, locale) { ... } export function validateEmail(email) { ... } export function fetchUser(id) { ... } export function formatDate(d, fmt) { ... } export function sendAnalytics(event) { ... } export function parseQueryParams(url) { ... } export function hashPassword(pw) { ... } export function generateToken(payload) { ... } // ... 1970 weitere Zeilen
✅ Nachher: Module-Struktur
// src/ // formatters/ // currency.ts // date.ts // index.ts ← Barrel // validators/ // email.ts // password.ts // index.ts // api/ // users.ts // index.ts // analytics/ // tracker.ts // index.ts // auth/ // tokens.ts // hashing.ts // index.ts // Bestehende Imports unverändert: import { formatCurrency, formatDate } from './utils'; // Barrel re-exportiert alles

Dependency-Inversion mit Interfaces

❌ Vorher: Enge Kopplung
// OrderService direkt von EmailService abhängig import { sendEmail } from './email-service'; class OrderService { async completeOrder(orderId: string) { const order = await getOrder(orderId); // Direkt gekoppelt — schwer testbar! await sendEmail(order.userEmail, 'Order complete'); } }
✅ Nachher: Dependency-Inversion
// Interface definiert Vertrag interface Notifier { notify(to: string, msg: string): Promise<void>; } class OrderService { constructor(private notifier: Notifier) {} async completeOrder(orderId: string) { const order = await getOrder(orderId); // Leicht austauschbar + testbar await this.notifier.notify( order.userEmail, 'Order complete' ); } } // Im Test: new OrderService(mockNotifier)

Feature-Sliced Design: Vertikal statt Horizontal

Statt Code nach technischen Schichten zu organisieren (alle Services zusammen, alle Models zusammen) gruppiert Feature-Sliced Design alles nach Business-Domänen. Das Ergebnis: ein Feature lässt sich als vollständige Einheit verstehen und ändern.

FSD

Feature-Sliced Verzeichnisstruktur

src/ features/ auth/ model/ ← State, Types api/ ← API-Calls ui/ ← Components lib/ ← Utils, Hooks index.ts ← Öffentliche API orders/ model/ api/ ui/ lib/ index.ts products/ ... shared/ ui/ ← Generische UI-Komponenten api/ ← HTTP-Client, Base-Config lib/ ← Domain-unabhängige Utils
Claude Code für Barrel-Export-Generierung:

"Erstelle für jedes Unterverzeichnis eine index.ts die alle öffentlichen Exporte re-exportiert. Erstelle dann eine Root-index.ts die alle Feature-Barrel-Exporte zusammenführt. Alle bestehenden Imports sollen ohne Änderung weiterhin funktionieren."

Fazit: Refactoring mit Claude Code — was wirklich funktioniert

Claude Code ist kein Zauberstab der Legacy-Code auf Knopfdruck modernisiert — aber als methodisch eingesetzter Refactoring-Partner ist es transformativ. Die entscheidenden Learnings aus der Praxis:

Legacy-Code modernisieren mit KI-Unterstützung

Teste Claude Code 14 Tage kostenlos und erlebe wie dein Team Refactoring-Projekte 3-5× schneller und sicherer abschließt.

Kostenlos starten — 14 Tage Trial

Weitere Artikel