Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Asynchronous API Refactoring with Claude Code

Hello! Welcome back to our series on advanced Claude Code workflows.

In our last session, you meticulously planned a sync-to-async refactoring for a FastAPI endpoint. You performed a detailed cross-file dependency analysis and created a sequential blueprint for the conversion. That was the "measure twice" phase—strategic, analytical, and crucial for de-risking the change.

Today, we "cut once." The learning outcome for this lesson is to guide Claude Code through an iterative refactor of a synchronous API route and its service functions to async equivalents. You'll take the plan you built and execute it, not with a single, risky command, but through a controlled, step-by-step process. This method of incremental refactoring with continuous verification is a cornerstone of professional AI-assisted development, ensuring each change is correct before moving to the next.

The Philosophy: Iterative Refactoring as a Dialogue

Why not just tell Claude, "Convert my FastAPI app to async"? As you know from your experience, large-scale, context-heavy tasks can cause AI assistants to fail if not managed properly. A more robust approach is to break the problem down and guide the agent through each step.

This aligns perfectly with Claude Code's internal process, which can be visualized as an iterative loop.

Claude Code: Agentic Coding Architecture and Iterative Reasoning Loop
This diagram shows how Claude Code operates in a cycle: it interprets a goal, makes a plan, executes an action, and observes the outcome. Our iterative prompting strategy hooks into this loop, providing verification and new instructions at each turn.

By feeding Claude one task at a time and verifying the result, you act as the "human-in-the-loop," ensuring the refactoring stays on track. This incremental method is not slower; it's faster because it prevents the costly process of untangling large, failed changes.

Let's explore the practical framework for this approach.

How to refactor 50k lines of legacy code without breaking prod

The following Reddit post, 'How to refactor 50k lines of legacy code without breaking prod,' offers a powerful real-world perspective on this exact workflow. It emphasizes the importance of control and verification.

Read the sections Step 2: Set Up Your CLAUDE .md File and Step 3: Incremental Refactoring With Continuous Verification. Focus on the core idea: breaking a large refactor into small, specific tasks, each with its own prompt and verification step. The prompt structure provided is a great template: "Implement this refactoring step: [specific task]. After making changes, run..."

Executing the Refactoring Plan: A Step-by-Step Guide

Now, let's execute the plan you created in the previous lesson. We will follow the sequence of changes required for a complete sync-to-async conversion in FastAPI. For each step, you will provide a precise prompt to Claude, review its changes, and then proceed.

The technical details of what needs to change are covered extensively in the video you reviewed previously. We'll use those details to craft our prompts.

Step 1: Refactor the Database Layer

The foundation of our async conversion is an async-aware database connection.

Your Prompt to Claude:
"Let's begin the sync-to-async refactoring based on our plan. Start with database.py.

  1. Update the SQLALCHEMY_DATABASE_URL to use an async driver (e.g., postgresql+asyncpg or sqlite+aiosqlite).
  2. Import create_async_engine, async_sessionmaker, and AsyncSession from sqlalchemy.ext.asyncio.
  3. Replace create_engine with create_async_engine.
  4. Replace sessionmaker with async_sessionmaker, configuring it for AsyncSession and setting expire_on_commit=False.
  5. Convert the get_db dependency into an async generator. It should use async with on the session factory to yield an AsyncSession instance."

After Claude makes the changes, use the diff view to confirm they are correct before moving on.

Step 2: Implement the Application Lifespan

FastAPI needs an async-aware way to handle startup and shutdown events, like creating database tables.

Your Prompt to Claude:
"Next, modify main.py. Remove the synchronous Base.metadata.create_all(bind=engine) call.

  1. Import asynccontextmanager from contextlib.
  2. Define an async def function decorated with @asynccontextmanager called lifespan.
  3. Before the yield, create an async connection with async with engine.begin() as conn: and run the synchronous table creation with await conn.run_sync(Base.metadata.create_all).
  4. After the yield, dispose of the engine with await engine.dispose().
  5. Assign this function to the app: app = FastAPI(lifespan=lifespan)."

This pattern ensures database setup happens within the async event loop.

Step 3: Refactor the Service and Repository Layers

This is the core of the logic change. Here, precision is key, especially regarding eager loading.

Python FastAPI Tutorial (Part 7): Sync vs Async - Converting Your App to Asynchronous

As a quick refresher, let's revisit the most critical and often-missed part of an async SQLAlchemy conversion: eager loading. Lazy loading is not supported and will cause runtime errors.

Briefly re-watch the segment from 13:05 to 14:44. The explanation of why lazy loading fails and how selectinload solves it is fundamental to crafting the next prompt.

Now, let's apply this knowledge.

Your Prompt to Claude:
"Now, refactor the service function that fetches a post.

  1. Change the function signature to async def.
  2. The call to the database execution method (e.g., session.execute(...)) must now be awaited.
  3. Based on our analysis, the Post model has related objects that are accessed. Modify the SQLAlchemy select statement to include .options(selectinload(models.Post.relationship_name)) to eagerly load these relationships.
  4. Remember to await calls to db.commit(), db.refresh(), and db.delete(), but do not await db.add()."

The prompt for this step is a great example of combining your domain knowledge (FastAPI/SQLAlchemy) with the refactoring plan to give Claude precise, executable instructions. The SitePoint article Hands-On with Claude Code demonstrates a similar incremental extraction process in its case study.

Step 4: Refactor the API Layer (Route Handler)

With the service layer now async, the final step is to update the API route that calls it.

Your Prompt to Claude:
"Finally, update the API route handler for GET /posts/{post_id} in the router file.

  1. Change the function signature to async def.
  2. The call to the underlying service function must now be awaited."

After this final change, your endpoint is fully asynchronous from the API entry point down to the database!

An Alternative: The /batch Command for Parallelizable Tasks

The iterative method is perfect for complex, sequential refactors. However, for tasks that are highly parallelizable and have few conflicts, Claude Code offers a more automated tool: the /batch skill.

Claude Code (New Updates): New SIMPLIFY & BATCH Skill Agents, Better UX, New Commands & MORE!

To broaden your toolkit, let's look at the /batch skill, which is designed for large-scale, parallel migrations.

Watch from 05:34 to 07:17. Note how /batch works by splitting tasks across multiple agents, each with its own isolated work tree. This is ideal for tasks like converting file formats or upgrading libraries across many independent components.

For example, a prompt like /batch migrate from CommonJS to ES modules in src/components would be a great use of this skill. For our sync-to-async conversion, which involves a deep, interconnected chain of dependencies, the iterative approach provides more control and is generally more suitable.

Conclusion

You have successfully executed a complex refactoring task by guiding Claude Code through a structured, iterative process. This workflow maximizes your control and minimizes risk, allowing you to leverage AI for significant codebase improvements safely.

Key Takeaways:

  • Iterative Execution is Key: Break down complex refactors into small, verifiable steps. This aligns with Claude's reasoning loop and prevents large-scale failures.
  • Prompt with Precision: Combine your domain expertise (e.g., knowing about selectinload) with the refactoring plan to give Claude unambiguous instructions for each step.
  • The Review -> Verify -> Commit Cycle: Treat Claude Code as a pair programmer. Review every change it makes, verify its correctness, and commit before moving on.
  • Choose the Right Tool: Use the iterative method for deep, sequential changes and consider the /batch skill for wide, parallelizable tasks.

Next Up:
You've made the changes and reviewed the code diff. But the ultimate verification comes from testing. In the next lesson, we will complete the refactoring loop by adapting our test suite. You will learn how to direct Claude Code to update corresponding unit tests to validate the migrated asynchronous API route, ensuring your refactored code is not only clean but also correct.

Can't find a good explanation? Sign up and we'll make it for you

Sign up