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:
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:
# 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:
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:
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:
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.
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.
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.
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:
# 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 filesThe 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.
# .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 patternThe "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:
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:
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:
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
-
Start with Plan Mode. Use Plan Mode to have Cursor research your codebase and create a structured migration plan before making any changes.
-
Pick a contained migration first. A single library upgrade or one module's conversion. Develop your workflow before tackling larger migrations.
-
Create a rules file. Document your target patterns and point to good examples. This investment pays off across every file Cursor touches.
-
Commit your examples. When you manually convert a file and get it right, commit it. Cursor uses it as a reference for subsequent conversions.