Skip to main content

Command Palette

Search for a command to run...

Modernizing Legacy Code with Cursor

Large-scale code migrations are tedious. Framework upgrades, language conversions, and refactoring projects can take months of mechanical find-and-replace work. Cursor handles these transformations across hundreds of files while you focus on the decisions that require developer judgment.

Framework and library upgrades

When you're a few versions behind on Angular, React, or any other framework, catching up means touching dozens or hundreds of files. Each version has breaking changes to navigate, and the work compounds when you've skipped multiple releases.

Cursor is well-suited for this work because it can search your codebase for the patterns you're migrating from and the patterns you're migrating to. It also has access to your terminal for running builds and tests, can read and write files across your project, and searches the web for current migration guides when it needs more context about breaking changes or new APIs.

How to approach it

Start with Plan Mode (Shift+Tab, then select Plan). Describe your migration and let Cursor research your codebase:

Plan mode example: Framework upgrade
Upgrade from Angular 12 to Angular 16. Identify all breaking changes we need to address.
Plan mode example: API migration
Find everywhere we use the old React context API and plan migration to the new one.
Agent example: Dependency audit
Audit our codebase for deprecated webpack plugins before upgrading to v5.

Cursor searches your code, asks clarifying questions, and creates a structured plan that you can review and edit before executing. Save plans to .cursor/plans/ to share with your team or reference later.

Execute in phases so that if something breaks, you know exactly which change caused it.

Define your target patterns with rules

Migrations go more smoothly when Cursor knows your conventions. Create a .cursor/rules/migration.mdc file that captures your decisions:

Rules example: Angular 16 Migration
# Angular 16 MigrationWhen updating Angular code:- Use standalone components, not NgModules- Use the new inject() function instead of constructor injection- Use signals for reactive state where possible- Keep existing naming conventions for components and servicesWhen updating RxJS:- Import operators from 'rxjs' not 'rxjs/operators'- Replace deprecated operators (switchMap stays, but pluck becomes map)

With rules files in place, Cursor follows your conventions consistently across every file it touches. This consistency matters most when you're changing hundreds of files.

Large-scale file transformations

Sometimes you need to make the same change across hundreds of files: changing import paths after a folder restructure, converting file extensions, or updating a deprecated API call everywhere it appears.

These changes are repetitive but not trivial. Each file may have slight variations in how the pattern appears, and a missed file can cause bugs that surface much later.

How Cursor handles bulk changes

When you ask Cursor to make changes across many files, it uses several tools together:

  • Codebase search to find affected files by exact text or related usage
  • File editing to apply changes while preserving surrounding code and formatting
  • Terminal access to run your build and test suite after changes, catching errors immediately
  • Web search to look up current documentation when it encounters unfamiliar APIs or edge cases

This means Cursor can find a deprecated API call, understand from online documentation how to convert it, make the change, run your tests, and fix any issues that come up.

Bulk file changes

Consider changing file extensions across a project:

Agent example: Bulk file extension change
Go through all files in src/components/. For any file that contains React components, change the extension from .js to .jsx. Update any imports that reference these files.

The key is being specific about scope and consequences:

  • Which files to look at
  • What condition triggers a change
  • What the change should be
  • What else needs updating as a result

Bulk API updates

When a library deprecates an API, you need to update every call site:

Agent example: API replacement
Find all uses of the deprecated moment() function. Replace them with dayjs() equivalents. The APIs are similar but not identical, so check each usage and convert it correctly.

Cursor finds each usage, understands the context, and applies the appropriate conversion rather than doing a blind find-and-replace.

Language and platform migrations

Moving from one stack to another is one of the most time-consuming projects a team can take on. JSP to React. .NET Framework to .NET Core. PHP to Node. These migrations traditionally take months because every file needs developer attention.

Converting between frameworks

For a JSP to React migration, you might use a prompt like:

Agent example: Framework conversion
Convert this JSP file to a React functional component. Preserve all the business logic and user interactions. Use our existing component library for UI elements (see src/components/ui/ for examples).

When Cursor processes this, it reads the JSP file to understand its structure and logic, references your existing React components to match your patterns, and generates a new component that preserves the original behavior. It can then update any files that referenced the old JSP template and run your test suite to verify the conversion.

Mainframe and COBOL migrations

Enterprise teams often face the challenge of modernizing mainframe systems written in COBOL. These migrations are particularly complex because the code may be decades old, documentation is often incomplete, and the developers who wrote it may no longer be available.

Ask mode example: Understand COBOL business logic
Analyze this COBOL program and explain its business logic. What data structures does it use? What are the main processing steps? Create documentation that would help someone unfamiliar with COBOL understand what this code does.

Start by having Cursor document the existing system. COBOL code is often self-documenting in structure but opaque in purpose. Cursor can read the code and explain the business rules it implements, the data it processes, and the edge cases it handles.

Agent example: Convert COBOL data structures
Convert this COBOL copybook to TypeScript interfaces. Preserve the field names and add comments explaining each field's purpose based on its usage in the codebase.

Data structure conversion is often the foundation of a mainframe migration. COBOL copybooks define record layouts that map to database tables and file formats. Converting these to modern type definitions preserves the institutional knowledge encoded in the original system.

Agent example: Convert COBOL to modern service
This COBOL program processes customer account updates. Convert it to a Node.js service that implements the same business logic. Preserve all validation rules and error handling. Add tests that verify the new implementation matches the COBOL behavior.

For the actual code conversion, work module by module rather than attempting the entire system at once. Each converted module can be tested against the original COBOL output to verify correctness. Create a rules file documenting COBOL-specific patterns and their modern equivalents:

Rules example: COBOL Migration Rules
# COBOL Migration Rules## Data type mappings- PIC 9(n) -> number (or BigInt for n > 15)- PIC X(n) -> string- PIC S9(n)V9(m) COMP-3 -> Decimal (use decimal.js library)## Common patterns- PERFORM VARYING -> for loop- EVALUATE TRUE -> switch statement or if-else chain- 88-level conditions -> enum or boolean properties## Business rules to preserve- Account numbers are always validated with check digit (see validateAccountNumber)- Date fields use YYYYMMDD format internally, convert to ISO 8601 for APIs- Negative amounts use trailing sign convention in files

The key to successful COBOL migrations is preserving business logic that has been refined over decades of production use. The code may look dated, but it often encodes valuable domain knowledge and edge case handling that must survive the migration.

Why this works

Cursor can find your existing patterns, your target patterns, and similar migrations elsewhere in your code.

When you convert the first few files and commit them, Cursor learns from those examples. Subsequent conversions become more accurate because it has reference implementations to follow. We've found that the first 10% of a migration might take longer than the remaining 90%.

Setting up context for migrations

For large migrations, a rules file helps Cursor make consistent decisions across hundreds of files. Without explicit guidance, Cursor might handle the same edge case differently in different files. A rules file ensures it follows your team's decisions everywhere.

Rules example: .NET Core Migration
# .NET Core Migration Rules## Patterns to follow- Use dependency injection via IServiceCollection- Replace HttpContext.Current with IHttpContextAccessor- Use async/await throughout, no .Result or .Wait()- Configuration via IOptions<T> pattern## Files to reference- src/Services/UserService.cs (already migrated, good example)- src/Controllers/BaseController.cs (shows new controller pattern)## Common gotchas- Session state works differently. See SessionHelper.cs for our approach- Our custom auth filter needs the new middleware pattern

The "files to reference" section is particularly useful: when Cursor encounters an ambiguous case, it looks at these examples to see how you've handled similar situations. The "common gotchas" section prevents Cursor from making mistakes you've already figured out how to avoid.

Refactoring at scale

Beyond version upgrades and language migrations, Cursor's Agent mode handles structural refactoring across large codebases.

Extracting shared code

When you have duplicated logic scattered across your codebase, you can ask Cursor to find it before proposing changes:

Agent example: Extract shared code
Look at how we handle API error responses across our codebase. Find all the places where we're doing similar error handling and extract a shared utility. Show me the patterns you find before making any changes.

Cursor searches your code, identifies the duplicated patterns, and proposes a shared abstraction that you can review before executing the extraction.

Applying patterns retroactively

You established a pattern six months ago, but older code still uses the old approach:

Agent example: Apply pattern retroactively
Our newer services use the Result pattern for error handling. Find services that still throw exceptions for business logic errors and convert them to use Result instead. Start with the UserService.

This kind of retroactive cleanup becomes practical when the mechanical work of finding and changing files takes minutes rather than hours.

Understanding before changing

For legacy code, understanding often matters more than changing. Use Ask mode to explore without making changes:

Ask mode example: Understand before refactoring
I need to refactor the PaymentProcessor class. Before we start, explain how it works, what its dependencies are, and what would break if we changed it.

Cursor traces through the code and identifies dependencies, giving you a clear picture before you commit to any changes. This is particularly useful when documentation is sparse or outdated.

Getting started

  1. Start with Plan Mode. Use Plan Mode to have Cursor research your codebase and create a structured migration plan before making any changes.

  2. Pick a contained migration first. A single library upgrade or one module's conversion. Develop your workflow before tackling larger migrations.

  3. Create a rules file. Document your target patterns and point to good examples. This investment pays off across every file Cursor touches.

  4. Commit your examples. When you manually convert a file and get it right, commit it. Cursor uses it as a reference for subsequent conversions.