Refactor a Legacy API with Claude Code

Refactor a messy TypeScript API using Claude Code's plan, checkpoint, and test workflow.

Introduction

30 Second Summary

Every developer eventually inherits code that works but is painful to maintain. Duplicated logic, missing types, and tangled responsibilities make every small change feel like a gamble.

In this project, you will use Claude Code to systematically refactor a messy TypeScript REST API, extracting clean service layers and adding proper types while a regression test suite proves nothing broke along the way.

What You'll Build

Picture running your full test suite after a sweeping refactoring and watching every test pass green, confirming your cleaner, well-typed codebase behaves identically to the messy original.

By the end of this project, you'll have:

  • A refactored Express API where route handlers are clean and slim, business logic lives in a dedicated service layer, and every function uses proper TypeScript types instead of any.
  • A regression test suite built with Vitest and supertest that you can run with one command to prove the refactored code behaves identically to the messy original.
  • A Git history demonstrating the disciplined checkpoint-and-verify rhythm that professionals use when refactoring production code.
  • Secret Mission: Identify a deeper architectural improvement with Claude Code, implement it without breaking any tests, and produce a professional refactoring report documenting the before and after state.

Are there any prerequisites?

This project assumes you already have Claude Code installed with an active subscription, plus Node.js/npm, Git, and VS Code on your machine.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up the Legacy Codebase

Every refactoring project starts the same way: with code that works but hurts to maintain. Before reaching for any tool to fix it, you need to see the mess firsthand.

In this step, you'll create a TypeScript/Express REST API for managing tasks. It handles CRUD operations and returns the right data. But under the hood, it's full of any types, duplicated validation logic, and business logic tangled directly into route handlers.

In this step, get ready to:
  • Create a Node.js project with TypeScript, Express, and testing dependencies.
  • Write the messy legacy API with duplicated validation and any types.
  • Initialize a Git repository with the legacy baseline committed.
Initialize the project and install dependencies
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open a terminal in VS Code by pressing Ctrl+` (backtick).
  • Move to your Desktop and create the project directory by running these commands:
cd ~/Desktop
mkdir legacy-task-api
  • Open this folder in VS Code:
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the legacy-task-api folder on your Desktop and click Open.

Trust the folder?

If VS Code asks whether you trust the authors of this folder, click Yes, I trust the authors.

  • Open a terminal in the project by pressing Ctrl+` (backtick).
  • Create package.json by clicking the New File icon in the Explorer sidebar.
  • Name the file package.json and paste the following content:
{
  "name": "legacy-task-api",
  "version": "1.0.0",
  "description": "Legacy task management API for refactoring practice",
  "main": "src/index.ts",
  "scripts": {
    "dev": "npx tsx src/index.ts",
    "test": "npx vitest run"
  },
  "dependencies": {
    "express": "5.2.1"
  },
  "devDependencies": {
    "typescript": "7.0.2",
    "tsx": "4.23.12",
    "vitest": "4.1.10",
    "supertest": "7.2.2",
    "@types/express": "5.0.2",
    "@types/supertest": "7.2.1"
  }
}

What does this file do?

This package.json declares the project's dependencies. express is the web framework. typescript and tsx handle TypeScript compilation and execution.

Vitest and supertest are testing tools you'll use in a later step to lock down the API's behavior before refactoring.

  • Save package.json.
  • Install all the dependencies by running this command in the terminal:
npm install

You should see output listing the installed packages, ending with a summary like added X packages.

Install failed?

  • Make sure you saved package.json before running npm install. Unsaved changes mean npm reads an empty or malformed file.
  • Check that your terminal's working directory is legacy-task-api. Run pwd to confirm.

Still stuck? Help me debug my npm install error in the legacy-task-api project.

Create the starter TypeScript files
  • Create a new file called tsconfig.json in the project root (click New File in the Explorer sidebar) and paste this content:
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "strict": false,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

Why is strict mode off?

Setting "strict": false is intentional. This simulates a real legacy codebase where TypeScript's strictest checks were never enabled. It lets any types and loose patterns slip through without compiler errors.

  • Create a folder called src by clicking the New Folder icon in the Explorer sidebar.
  • Inside the src folder, create a file called index.ts and paste this content:
import { app } from "./app";

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

What does this file do?

This is the entry point that starts the server. It imports the Express app from app.ts (which you'll create next) and listens on port 3000.

  • Save tsconfig.json and src/index.ts.
  • Confirm the files exist by running:
ls

You should see node_modules, package.json, package-lock.json, src, and tsconfig.json in the output.

Write the legacy API code

Now for the main event. You'll create src/app.ts with every route handler crammed into one file. This is the messy code you'll refactor in later steps.

  • Inside the src folder, create a file called app.ts and paste the initial setup code:
import express from "express";

const app = express();
app.use(express.json());

// In-memory storage
let tasks: any[] = [];
let nextId = 1;

// Helper that gets duplicated in multiple places
function formatDate(date: any) {
  return date.toISOString();
}

// GET all tasks
app.get("/tasks", (req: any, res: any) => {
  try {
    res.json(tasks);
  } catch (err) {
    res.status(500).json({ error: "Something went wrong" });
  }
});
  • Now add the export lines at the very bottom of the file. These let index.ts import the app:
// Export for testing - but also used by index.ts
export { app, tasks, nextId };

// Reset function for tests
export function resetTasks() {
  tasks.length = 0;
  nextId = 1;
}

What's messy here already?

Notice any on the tasks array, the formatDate helper, and both handler parameters. TypeScript's type system is completely bypassed. The try/catch in every handler is also boilerplate that could be centralized.

  • Save src/app.ts.
  • Start the server to confirm everything wires up by running:
npx tsx src/index.ts

You should see Server running on port 3000 in the terminal.

  • Open a second terminal tab by pressing Ctrl+Shift+` (macOS: Cmd+Shift+`).
  • Test the GET endpoint by running:
curl http://localhost:3000/tasks

You should see [] returned. The API is alive with an empty task list.

  • Switch back to the first terminal tab and stop the server by pressing Ctrl+C.

Server not starting?

  • If you see a module-not-found error, make sure src/app.ts has the export { app, tasks, nextId } line at the bottom. The index.ts file imports app from it.
  • If port 3000 is already in use, stop any other servers running on that port first.

Still stuck? Help me debug why my Express server won't start in the legacy-task-api project.

Now you'll add the remaining route handlers. Each one goes in src/app.ts above the export lines at the bottom of the file.

  • In src/app.ts, find the closing }); of the GET /tasks handler. Add this code below it, above the export lines:
// GET single task
app.get("/tasks/:id", (req: any, res: any) => {
  try {
    const id = parseInt(req.params.id);
    const task = tasks.find((t: any) => t.id === id);
    if (!task) {
      res.status(404).json({ error: "Task not found" });
      return;
    }
    res.json(task);
  } catch (err) {
    res.status(500).json({ error: "Something went wrong" });
  }
});

What does this handler do?

This fetches a single task by ID. Notice the same pattern: any on every parameter, a manual parseInt call, and the identical try/catch wrapper.

  • Below the GET /tasks/:id handler, add the POST handler. Start with the validation logic:
// POST create task
app.post("/tasks", (req: any, res: any) => {
  try {
    const body = req.body;

    // Validation - duplicated logic
    if (!body.title || typeof body.title !== "string") {
      res.status(400).json({ error: "Title is required and must be a string" });
      return;
    }

    if (body.title.trim().length === 0) {
      res.status(400).json({ error: "Title is required and must be a string" });
      return;
    }

    if (body.title.length > 200) {
      res.status(400).json({ error: "Title must be 200 characters or less" });
      return;
    }
  • Add the task creation logic directly below to complete the POST handler:
    const task: any = {
      id: nextId++,
      title: body.title.trim(),
      description: body.description || "",
      status: "pending",
      createdAt: formatDate(new Date()),
      updatedAt: formatDate(new Date()),
    };

    tasks.push(task);
    res.status(201).json(task);
  } catch (err) {
    res.status(500).json({ error: "Something went wrong" });
  }
});

Spot the duplication?

The validation checks here (missing title, empty string, length limit) will appear again almost identically in the PUT handler. The comment // Validation - duplicated logic even calls it out. This duplication is exactly what you'll extract into a shared function later.

  • Save src/app.ts.
  • Below the POST handler, add the PUT handler. Start with the validation section:
// PUT update task
app.put("/tasks/:id", (req: any, res: any) => {
  try {
    const id = parseInt(req.params.id);
    const taskIndex = tasks.findIndex((t: any) => t.id === id);

    if (taskIndex === -1) {
      res.status(404).json({ error: "Task not found" });
      return;
    }

    const body = req.body;

    // Validation - duplicated from POST
    if (body.title !== undefined) {
      if (typeof body.title !== "string") {
        res.status(400).json({ error: "Title is required and must be a string" });
        return;
      }
      if (body.title.trim().length === 0) {
        res.status(400).json({ error: "Title is required and must be a string" });
        return;
      }
      if (body.title.length > 200) {
        res.status(400).json({ error: "Title must be 200 characters or less" });
        return;
      }
    }
  • Add the update logic directly below to complete the PUT handler:
    if (body.status !== undefined) {
      // Duplicated status validation
      const validStatuses = ["pending", "in-progress", "completed"];
      if (!validStatuses.includes(body.status)) {
        res.status(400).json({
          error: "Status must be one of: pending, in-progress, completed",
        });
        return;
      }
    }

    const existingTask = tasks[taskIndex];
    const updatedTask: any = {
      ...existingTask,
      title: body.title !== undefined ? body.title.trim() : existingTask.title,
      description:
        body.description !== undefined
          ? body.description
          : existingTask.description,
      status:
        body.status !== undefined ? body.status : existingTask.status,
      updatedAt: formatDate(new Date()),
    };

    tasks[taskIndex] = updatedTask;
    res.json(updatedTask);
  } catch (err) {
    res.status(500).json({ error: "Something went wrong" });
  }
});

The validation problem is worse here

The comment // Validation - duplicated from POST says it all. The title validation is nearly identical to the POST handler. The status validation adds its own block of checks. Every route also builds its own any-typed objects by hand.

  • Save src/app.ts.
  • Restart the server to test the new routes. Switch to the first terminal tab and run:
npx tsx src/index.ts
  • Switch to your second terminal tab and test creating a task by running:
curl -X POST http://localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"Test task"}'

Windows PowerShell users

PowerShell aliases curl to a different command. Use curl.exe instead. You also need to escape inner double quotes differently. The simplest fix is to run these commands in Git Bash instead, which supports the same syntax as macOS and Linux.

You should see a JSON response with an id, title, status set to pending, and timestamps.

  • Stop the server by pressing Ctrl+C in the first terminal tab.

POST returning an error?

  • Make sure the POST handler code is placed above the export lines, not below them.
  • Check that the closing }); of the POST handler's app.post(...) is present. A missing bracket causes a syntax error.

Still stuck? Help me figure out why my POST /tasks endpoint is not working.

  • Add the final route handler for DELETE below the PUT handler, still above the export lines:
// DELETE task
app.delete("/tasks/:id", (req: any, res: any) => {
  try {
    const id = parseInt(req.params.id);
    const taskIndex = tasks.findIndex((t: any) => t.id === id);

    if (taskIndex === -1) {
      res.status(404).json({ error: "Task not found" });
      return;
    }

    tasks.splice(taskIndex, 1);
    res.status(204).send();
  } catch (err) {
    res.status(500).json({ error: "Something went wrong" });
  }
});

Same pattern, same problems

Even the simplest handler follows the same bloated pattern: any types, manual parseInt, hand-rolled not-found check, and the identical try/catch wrapper. Five handlers, five copies of the same structure.

  • Save src/app.ts.

✔️ Awesome, I've got everything!

Great. Double check you've saved src/app.ts and confirm the file is error-free in VS Code (no red underlines in the editor).

ⓧ I'd like to double check the full code

Your src/app.ts should contain these sections in order. Scroll up and confirm each one is present:

  • Imports, Express setup, in-memory storage (tasks array, nextId), and the formatDate helper function.
  • GET /tasks handler returning all tasks.
  • GET /tasks/:id handler returning a single task.
  • POST /tasks handler with title validation and task creation.
  • PUT /tasks/:id handler with duplicated validation and update logic.
  • DELETE /tasks/:id handler.
  • Export lines: export { app, tasks, nextId } and the resetTasks function at the very bottom.
  • Initialize a Git repository and commit the legacy baseline by running:
git init
git add -A
git commit -m "initial legacy codebase"

You should see output listing the files committed, confirming the baseline is saved.

Why commit now?

This commit captures the messy original code as a snapshot. Later, after refactoring, you'll use git diff to compare the clean version against this baseline and see exactly what changed.

Run the API and confirm it works

The messy code is committed. Before moving on, you need to verify that the API works end to end. A clean refactoring starts from a known-good state.

  • Start the server by running:
npx tsx src/index.ts

Before testing, what do you expect the GET /tasks endpoint to return right after a fresh server start?

  • Switch to your second terminal tab and test the GET endpoint by running:
curl http://localhost:3000/tasks

You should see []. The in-memory store starts empty on every server restart.

  • Test creating a task by running:
curl -X POST http://localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"Test task"}'

You should see a JSON response with id set to 1, title set to Test task, status set to pending, and createdAt/updatedAt timestamps.

Seeing connection refused?

  • Make sure the server is running in your first terminal tab. You should see Server running on port 3000 there.
  • Confirm you're using http://localhost:3000 (not https) in your curl command.

Still stuck? Help me debug why curl cannot connect to my Express server on port 3000.

  • Stop the server by pressing Ctrl+C in the first terminal tab.

Your legacy codebase is committed and confirmed working. Next up, you'll launch Claude Code to explore the mess and build a mental map before changing a single line.

Explore the Codebase with Claude Code

Your legacy API is up and running, and you have a clean Git baseline to fall back to. Before touching a single line of code, you need to understand what's actually wrong with it.

Jumping straight into refactoring unfamiliar code is how you break things. Claude Code can read your entire codebase and map out every problem, letting you build a mental model before any edits happen.

In this step, get ready to:
  • Launch Claude Code and explore the codebase using targeted archaeology prompts.
  • Use Plan Mode to produce a read-only refactoring strategy.
Launch Claude Code and explore the codebase

The first rule of safe refactoring is to understand the code before you change it. Claude Code can read every file in your project and answer targeted questions about structure, patterns, and problems.

  • Return to your terminal where the server is still running from Step 1.
  • Press Ctrl+C to stop the server.
  • Launch Claude Code in your project directory by running:
claude

Claude Code starts up and shows its interactive prompt. You can now type natural language questions about your project directly at this prompt.

Command not found?

If claude is not recognized, Claude Code may not be installed or may not be on your PATH. Follow the official installation guide to set it up.

Help me install Claude Code

  • Ask Claude Code for a high-level overview of the codebase by typing this at the prompt:
give me an overview of this codebase

Claude Code reads through your files and produces a summary of the project structure, the routes, and the in-memory storage pattern. This is your starting map.

What is codebase archaeology?

Codebase archaeology is the practice of exploring unfamiliar code to understand its structure before making changes. Instead of guessing how things connect, you systematically trace patterns and dependencies.

Claude Code makes this faster because it can read your entire project at once and answer specific questions. Manually searching through files would take much longer to build the same picture.

  • Now dig into specific problems. Ask Claude Code to find duplicated logic by typing:
find all the places where validation logic is duplicated

Claude Code highlights how the POST and PUT route handlers both contain the same title validation checks, copy-pasted with minor differences.

  • Ask about type safety by typing:
what types are using any?

Claude Code finds any types scattered across the file. Every request handler, every response handler, the tasks array, and even the formatDate helper all use any instead of proper TypeScript interfaces.

  • Trace the error handling patterns by typing:
trace how error handling works across the routes

Claude Code shows how every route handler wraps its logic in its own try/catch block, each returning a generic "Something went wrong" message. There is no centralized error handling.

Switch to Plan Mode and get a refactoring strategy

You now know what's broken. The next move is to get a structured plan for fixing it, without Claude Code changing any files yet.

  • Press Shift+Tab twice to cycle through modes until the status bar at the bottom of the Claude Code interface shows plan mode on.

What is Plan Mode?

Plan Mode is a read-only mode in Claude Code. When it's active, Claude Code analyzes your code and proposes changes but does not modify any files. This lets you review a strategy before committing to any edits.

Pressing Shift+Tab cycles through three permission modes: default, acceptEdits, and plan. You can also launch Claude Code directly in Plan Mode by running claude --permission-mode plan.

  • Ask Claude Code to create a refactoring plan by typing:
analyze this codebase and create a refactoring plan that extracts a service layer, adds proper TypeScript types, and centralizes error handling. Don't change any existing API behavior.

Claude Code produces a detailed, ordered plan covering the sequence of refactoring changes. It describes what files to create, what logic to move, and how to structure the new types and error handling.

  • Read through the plan Claude Code proposes. Notice the order of operations: types first, then service extraction, then error middleware, then route handler cleanup.
  • Press Shift+Tab to cycle back out of Plan Mode.

Why plan before editing?

AI tools can generate code fast, but speed without direction leads to messy refactors. Reviewing a plan first lets you catch questionable decisions (like accidentally changing API behavior) before they become real changes you need to undo.

Before you move on, test your understanding. Can you name at least three specific problems Claude Code identified in the codebase?

What should I be able to name?

From Claude Code's analysis, you should be able to identify: duplicated validation logic between POST and PUT handlers, any types used everywhere instead of proper TypeScript interfaces, and business logic tangled directly into route handlers with no service layer separation.

You have a clear map of the mess and a structured plan to clean it up. Next, you'll write regression tests that lock down every current behavior, creating the safety net that proves your refactoring doesn't break anything.

Write Regression Tests to Lock Down Behavior

You now have a clear mental map of the codebase's problems: duplicated validation, any types everywhere, and business logic tangled into route handlers.

Before you change a single line of code, you need proof that the API works exactly as it does right now. Regression tests capture every response status code, error message, and edge case in automated checks. When you refactor later, these tests will catch any accidental changes instantly.

In this step, get ready to:
  • Ask Claude Code to generate a comprehensive regression test suite with Vitest and supertest.
  • Run the tests and confirm they all pass against the unchanged legacy code.
  • Commit the test suite to Git as your safety net.
Ask Claude Code to generate a regression test suite

The API currently handles tasks with GET, POST, PUT, and DELETE endpoints. Each endpoint has validation rules, specific error messages, and exact status codes. A regression test suite locks down all of this behavior so you can prove nothing changed after refactoring.

What are regression tests?

Regression tests are automated checks that capture a system's current behavior. They do not test whether the behavior is correct. They test whether the behavior stays the same after you make changes.

If a test passes before the refactoring and fails after, something changed. That change might be a bug you introduced, or it might be a deliberate improvement. Either way, the test forces you to acknowledge it.

  • In your Claude Code session, type this prompt to generate the test suite:
write a comprehensive regression test suite using vitest and supertest that tests every endpoint in app.ts. Cover: listing tasks, creating tasks with valid/invalid data, updating tasks, deleting tasks, and error cases like missing fields and non-existent IDs. Export the app from app.ts without starting the server so supertest can use it.

What will Claude Code do?

Claude Code reads every route handler in app.ts, identifies the validation rules and error responses, and generates two new files:

  • vitest.config.ts in the project root, which configures Vitest with global test functions.
  • src/app.test.ts, which contains test cases for every endpoint: GET, POST, PUT, and DELETE. It covers success paths, missing fields, invalid data, whitespace handling, character limits, and 404 responses.

Claude Code will also check that app.ts exports the Express app without calling app.listen(). Your codebase is already structured this way, so no changes are needed there.

Allow file creation

Claude Code will show a diff preview of each file it wants to create or modify. Review the changes and approve them when prompted. You should see new files being created and possibly minor adjustments to existing exports.

  • After Claude Code finishes, confirm both files were created by running:
ls vitest.config.ts src/app.test.ts

You should see both file paths printed without errors.

✔️ I see both files listed

Great. Your test suite is in place. Time to run it.

ⓧ A file is missing or I see an error

Claude Code may not have created both files on the first attempt. Try a follow-up prompt to address the gap:

  • If vitest.config.ts is missing, ask Claude Code: "create a vitest.config.ts with globals enabled".
  • If src/app.test.ts is missing, ask Claude Code: "create the test file at src/app.test.ts".

Run the ls command again to confirm both files now exist.

Run the tests and confirm they all pass

The test file is ready, but you have not changed any application code. Before you run the tests, think: do you expect them to pass or fail against the original legacy code?

  • Open a separate terminal window. You can keep Claude Code running in the existing one.
  • Navigate to your legacy-task-api project folder and run the test suite:
npx vitest run

Every test passes green. The summary at the bottom shows all test files and individual tests passing with zero failures.

Why do they all pass?

The tests were written against the code as it exists right now. Every assertion (status codes, error messages, response shapes) was derived directly from the current route handlers. The tests are a snapshot of current behavior, not a specification of ideal behavior.

This is the power of regression tests. They give you a baseline to compare against. If any test fails after refactoring, you know exactly which behavior changed and can decide whether the change was intentional.

Seeing test failures?

If tests fail at this stage, Claude Code's generated assertions may not exactly match the legacy code's behavior. Go back to your Claude Code session and share the failure output. Ask: "these tests fail against the current code, fix the assertions to match the actual behavior." Claude Code will read the error output and adjust the tests.

If you see a module resolution error, check that vitest.config.ts exists in the project root (not inside src/).

Still stuck? Help me debug failing Vitest tests in my legacy-task-api project.

With all tests passing, this is the perfect moment to save a checkpoint in Git. If anything goes wrong during refactoring, you can always return to this known-good state.

  • Commit the test suite and config to Git by running:
git add -A && git commit -m "add regression tests"

You should see a commit summary listing the new files: vitest.config.ts and src/app.test.ts.

Commit showing unexpected file changes?

If the commit includes changes to app.ts or index.ts, that means Claude Code made minor adjustments to the exports. This is fine as long as all your tests still pass. Run npx vitest run one more time to confirm.

Need help? Help me understand the unexpected file changes in my git commit.

Your safety net is in place. Every endpoint, every validation rule, and every error response is now captured in an automated test suite. Next up, you will use Claude Code to refactor the messy codebase. The tests will be watching.

Refactor with Claude Code and Hit the Breakage

You've mapped the mess, planned the fix, and locked down every behavior with regression tests. Now it's time for the real refactoring.

In this step, you'll hand the refactoring to Claude Code and let it restructure the entire codebase across multiple files in a single pass. Then you'll find out whether the changes preserved every existing behavior.

In this step, get ready to:
  • Ask Claude Code to extract a service layer, add TypeScript types, and centralize error handling.
  • Run your regression tests to check whether the refactoring changed any behavior.
Ask Claude Code to perform the first refactoring pass

The whole point of exploring and planning first is so Claude Code has full context when it starts editing. Now you'll give it a single, detailed prompt that covers all three refactoring targets at once.

  • Switch back to Claude Code and enter this prompt:
refactor this codebase following the plan. Extract a TaskService class into src/services/taskService.ts with proper TypeScript interfaces. Move all business logic out of the route handlers. Add a Task interface and use it everywhere instead of any. Centralize error handling with an Express error middleware.

What does this prompt ask Claude Code to do?

This prompt asks Claude Code to split the tangled app.ts into multiple focused files.

  • src/types.ts will hold a Task interface replacing every any type.
  • src/services/taskService.ts will hold all business logic currently jammed into route handlers.
  • src/middleware/errorHandler.ts will replace the duplicated try/catch blocks with a single Express error middleware.
  • src/app.ts will be rewritten to thin route handlers that delegate to the service and middleware.
  • Claude Code will ask for permission to create and edit files. Allow the edits.

Watch Claude Code's output as it creates the new files and rewrites app.ts to use them. It reports that the code compiles without errors.

  • Confirm the new files were created by running this in a separate terminal:
ls src/ src/services/ src/middleware/

You should see types.ts alongside your existing files in src/, taskService.ts inside src/services/, and errorHandler.ts inside src/middleware/.

Don't see the new files?

  • Make sure you allowed Claude Code's file creation permissions when prompted. If Claude Code skipped creating a file, re-enter the refactoring prompt and accept all edits.
  • Check that you're running ls from the project root (the legacy-task-api folder).

Claude Code didn't create all the expected files during refactoring

Run the regression tests and observe the result

The refactoring is done and the code compiles cleanly. Claude Code didn't report any TypeScript errors. Three new files, a rewritten app.ts, and zero compiler complaints.

Before you run the tests, take a moment to predict. The code compiles without errors. Do you think all your regression tests will still pass, or will the refactoring have changed something?

  • Run the test suite by running:
npx vitest run

At least one test fails.

Read the test failure output carefully. A test expected one error response shape, and the refactored code produced another. During the refactoring, the error response for a specific edge case shifted. Maybe a validation that used to return a 400 now returns a 500, or the error message format changed when error handling moved to the centralized middleware.

Why did the tests catch this?

The original code had validation logic inlined with specific error messages and status codes. When Claude Code extracted that logic into a service layer and centralized error handling, the exact error response for an edge case shifted.

The code works fine for happy-path requests. The breakage lives in the edge cases where error format matters. This is exactly why you wrote regression tests first.

This result is intentional. The code compiles, looks cleaner, and handles the main paths correctly. But the regression tests prove that refactoring is not just about making code look better. It's about preserving every existing behavior.

Your tests just caught a real behavioral regression. In the next step, you'll use Claude Code's checkpoint system to rewind the damage and fix the root cause properly.

Recover with Checkpoints and Fix Properly

Your regression tests just saved you. The refactoring in the last step compiled cleanly, but the test suite caught a subtle behavioral change that would have slipped into production unnoticed.

Starting over from scratch would throw away useful analysis work. Claude Code's checkpoint system lets you rewind to the exact moment before the breakage, then re-apply the refactoring with a targeted fix. This is the core safety workflow for refactoring production code.

In this step, get ready to:
  • Rewind to the pre-refactoring checkpoint using Claude Code's /rewind command.
  • Re-apply the refactoring with an explicit constraint to preserve error behavior.
  • Review the full diff and commit the clean refactoring.
Use Claude Code's checkpoint system to recover

Claude Code automatically captures a checkpoint before each prompt you send. This means there is a snapshot of your entire codebase from right before you asked for the refactoring.

You can jump back to that exact state without manually undoing anything.

  • In Claude Code, type /rewind and press Enter to open the checkpoint list.

You will see a list of timestamps, each tied to a prompt you sent during this session.

  • Select the checkpoint from just before your refactoring prompt (the one where you asked Claude Code to extract a service layer).
  • Choose Restore code and conversation from the options menu.

What does Restore code and conversation do?

This option reverts every file on disk to the state it was in at that checkpoint AND resets the Claude Code conversation to that point. It is the most thorough rewind option.

Other options like Restore code only revert files without touching the conversation history.

Your files are now back to the state before the refactoring. The broken src/types.ts, src/services/taskService.ts, and src/middleware/errorHandler.ts files are gone, and src/app.ts is the messy original again.

Before you run the tests, what do you expect to see? You just rewound to the pre-refactoring state where all tests were passing.

  • Confirm the rewind worked by running the tests in a separate terminal:
npx vitest run

All tests pass green. You are back to the safe state from the end of Step 3.

Tests still failing after rewind?

  • Make sure you selected the checkpoint from before the refactoring prompt, not after it. The timestamps in the /rewind list show when each checkpoint was taken.
  • Confirm you chose Restore code and conversation, not just Restore conversation. The latter does not revert files on disk.
  • Why are my tests still failing after using /rewind in Claude Code?
Re-do the refactoring with the fix included

The first refactoring attempt changed error response behavior without anyone asking for it. This time, you will tell Claude Code exactly what to preserve.

  • In Claude Code, enter this prompt:
refactor this codebase following the plan, but ensure that error responses for invalid input (like missing title) return exactly the same status codes and error message format as the original code. The regression tests must continue to pass.

Why does this constraint matter?

The original failure happened because Claude Code improved the error handling during refactoring. The new error middleware shaped responses differently than the inline handlers did.

By stating the constraint up front, Claude Code knows to match the original error format character for character. This is how production refactoring works. You specify exactly which behaviors must be preserved.

Claude Code creates the same new files as before and rewrites src/app.ts. This time, the error responses match the original API behavior exactly. Claude Code will ask for permission to edit files as it works through the changes.

Before you run the tests, do you think they will pass this time? The refactoring included an explicit constraint to preserve error behavior.

  • Run the regression tests to confirm the fix worked:
npx vitest run

All tests pass green. The refactoring preserved every original behavior while cleaning up the architecture.

Tests still failing after the constrained refactoring?

  • Read the failing test output carefully. It tells you exactly which status code or error message changed.
  • Copy the failing test output and paste it into Claude Code with the prompt: "this test is still failing after the refactoring. Fix the issue so the test passes without changing the test."
  • Claude Code will read the test assertion, compare it to the current code, and adjust the specific file causing the mismatch.
  • How do I fix remaining test failures after re-doing the Claude Code refactoring?
Review the changes and commit

Before committing, you want to see exactly what changed across the entire codebase. This is the final quality check before the refactoring becomes part of the Git history.

  • In Claude Code, ask for a summary of the refactoring:
show me a summary of all files changed and what each change accomplishes

Claude Code lists every file it created or modified, with a description of the purpose of each change.

  • Switch to your separate terminal and review the full diff from the legacy baseline by running:
git diff

What should I see in the diff?

The diff shows every line added and removed since your initial legacy commit. You should see:

  • New files: src/types.ts with TypeScript interfaces, src/services/taskService.ts with extracted business logic, and src/middleware/errorHandler.ts with centralized error handling.
  • A rewritten src/app.ts that imports from the service and middleware instead of containing everything inline.
  • The any types replaced with proper TypeScript interfaces throughout.
  • Commit the clean refactoring by running:
git add -A && git commit -m "refactor: extract service layer, add types, centralize errors"

Why this commit message format?

The refactor: prefix follows the Conventional Commits convention. It signals that this change restructures code without altering external behavior.

Anyone reading the git history instantly knows this commit is safe to review quickly because it should not introduce new features or break existing ones.

You should see a confirmation message listing the files changed and insertions/deletions. Your refactoring is now part of the permanent Git history.

Git commit failed?

  • If you see "nothing to commit, working tree clean", the files may not have been saved. Return to Claude Code and ask it to confirm all files are written to disk.
  • If you see an error about git configuration, you need to set your identity first. Run git config user.email and git config user.name with your details, then retry the commit.
  • My git commit is failing after the Claude Code refactoring. What should I check?

Your refactored codebase is clean, tested, and committed. The messy legacy API now has proper types, separated concerns, and centralized error handling. If you are ready for a deeper challenge, the Secret Mission asks you to identify an architectural improvement and produce a professional refactoring report.

Secret mission

Architectural Improvement and Refactoring Report

The refactoring cleaned up duplicated validation, scattered any types, and tangled route handlers. A deeper structural problem still lurks in the codebase. Can you identify an architectural improvement, implement it without breaking a single test, and produce a professional refactoring report documenting the entire journey?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing cloud costs or paid services to worry about. Decide whether to keep your project files for future reference, or delete them entirely.

Resources you used:

  • The legacy-task-api project folder on your Desktop (source code, tests, node_modules, git history, and REFACTORING_REPORT.md).

Keep everything running

No action needed. Choose this if you want to revisit the refactored codebase, extend the API, or reference your refactoring report later.

  • Your legacy-task-api folder stays on your Desktop with all source code, tests, git history, and your REFACTORING_REPORT.md.
  • The node_modules folder takes up disk space. You can always regenerate it later by running npm install inside the project folder.

Pause - I'll come back to this later

Free up disk space by removing the installed packages while keeping your source code and git history intact.

  • Delete the node_modules folder to reclaim disk space. On macOS/Linux, run:
cd ~/Desktop/legacy-task-api
rm -rf node_modules
  • On Windows, run:
cd ~\Desktop\legacy-task-api
rmdir /s /q node_modules
  • When you come back, run npm install inside the project folder to restore everything.

Delete - I don't want to use this again

Remove the entire project folder and all its contents from your machine.

  • Delete the legacy-task-api folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf legacy-task-api
  • On Windows, run:
cd ~\Desktop
rmdir /s /q legacy-task-api
  • Confirm the folder is gone by running:
ls ~/Desktop
  • You should no longer see legacy-task-api in the output.

Nice Work!

Nice Work!

You just took a messy, tangled TypeScript API and systematically refactored it into a clean, well-structured codebase without breaking a single existing behavior. That is exactly how professionals handle legacy code in production.

What you learned:

  • Explored unfamiliar code using Claude Code's codebase archaeology and Plan Mode, building a mental model and a read-only refactoring strategy before touching a single line.
  • Wrote a regression test suite with Vitest and supertest that locked down every API behavior, then used it to catch a subtle breakage that compiled fine but changed the API's responses.
  • Performed multi-file refactoring to extract a service layer, add TypeScript interfaces, and centralize error handling. Then recovered from the designed breakage using Claude Code's checkpoint system and verified every change with git diff.
  • Secret Mission: Identified a deeper architectural improvement, implemented it with all tests still passing, and produced a professional refactoring report documenting the before and after state.

Ready to quiz yourself?