10 Code Migration Prompts: Python 2→3, JavaScript→TypeScript, REST→GraphQL

10 Code Migration Prompts: Python 2→3, JavaScript→TypeScript, REST→GraphQL

Migrating code between versions or technologies is one of the most delicate tasks in software engineering. A single oversight can break production, introduce subtle bugs, or double your technical debt. While automated tools handle the low-hanging fruit (syntax changes, type annotations), the strategic decisions—when to refactor, how to preserve semantics, what to deprecate—still require human expertise. Or, increasingly, a well-crafted prompt for an AI coding assistant.

This article collects 10 proven prompts for three common migration scenarios: Python 2 to 3, JavaScript to TypeScript, and REST to GraphQL. Each prompt is designed to guide an AI (like GPT-4o, Claude 3.5 Sonnet, or Gemini 1.5 Pro) toward generating safe, idiomatic, and production-ready code. We'll include example outputs, explain the reasoning behind each prompt, and highlight pitfalls to avoid.

Why Prompt Engineering Matters for Migration

Migration isn't just translation. It's about preserving intent. A naive prompt like "Convert this Python 2 code to Python 3" might produce syntactically correct code that still uses deprecated libraries or ignores performance improvements. The prompts below are structured with context, constraints, and examples to ensure the AI understands the migration's goal.

Migration Type Key Challenges Prompt Focus
Python 2→3 Unicode handling, print function, division, standard library renames Explicitly handle unicode, xrange, and iteritems
JavaScript→TypeScript Implicit any, dynamic object shapes, complex generics Require strict mode and explicit return types
REST→GraphQL Over-fetching, under-fetching, endpoint redesign Generate resolvers that match existing REST endpoints

Python 2 to 3 Migration Prompts

1. Basic Syntax Migration with Modern Idioms

Task: Convert a legacy Python 2 module to Python 3, replacing deprecated constructs with their idiomatic Python 3 equivalents.

Prompt:

You are a senior Python developer migrating a codebase from Python 2.7 to Python 3.10+. Convert the following Python 2 code to Python 3, following these rules:
1. Replace `print` statement with `print()` function.
2. Replace `xrange()` with `range()`.
3. Replace `dict.iteritems()` with `dict.items()`.
4. Use Python 3's `//` for integer division where floor division is intended.
5. Replace `unicode` with `str` and handle bytes explicitly with `b''`.
6. Replace `basestring` with `str`.
7. Use f-strings instead of `%` formatting where readability improves.
8. Keep the same function signatures and module-level structure.

Python 2 code:
```python
# legacy_utils.py
import urllib2
import urlparse

def fetch_data(url):
    request = urllib2.Request(url)
    response = urllib2.urlopen(request)
    return response.read()

def process_items(items):
    result = []
    for key, value in items.iteritems():
        if isinstance(value, unicode):
            value = value.encode('utf-8')
        result.append((key, value))
    return result

def calculate_average(numbers):
    total = sum(numbers)
    return total / len(numbers)
**Example Result:**
```python
# legacy_utils.py (Python 3)
import urllib.request
import urllib.parse

def fetch_data(url):
    request = urllib.request.Request(url)
    response = urllib.request.urlopen(request)
    return response.read()

def process_items(items):
    result = []
    for key, value in items.items():
        if isinstance(value, str):
            value = value.encode('utf-8')
        result.append((key, value))
    return result

def calculate_average(numbers):
    total = sum(numbers)
    return total / len(numbers)  # returns float; use // for int

2. Safe Migration of Exception Handling

Task: Update exception syntax that changed between Python 2 and 3.

Prompt:

Convert the following Python 2 exception handling to Python 3. In Python 3:
- Use `except Exception as e:` instead of `except Exception, e:`.
- Use `raise Exception('message') from original_error` to chain exceptions.
- The `sys.exc_info()` is still valid but prefer `traceback.format_exc()` for logging.

Python 2 code:
```python
def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError, e:
        print "Cannot divide by zero:", e
        return None
**Example Result:**
```python
def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError as e:
        print(f"Cannot divide by zero: {e}")
        return None

3. Migration of Unicode and Bytes Handling

Task: Rewrite a Python 2 module that mixes unicode and str to use Python 3's clear bytes/str separation.

Prompt:

Migrate this Python 2 code to Python 3. In Python 3:
- All strings are Unicode by default.
- Use `b''` for bytes literals.
- Replace `unicode()` with `str()`.
- Use `.encode()` to convert str to bytes, `.decode()` to convert bytes to str.
- Replace `isinstance(s, basestring)` with `isinstance(s, str)`.

Python 2 code:
```python
def normalize_text(text):
    if isinstance(text, unicode):
        return text.lower()
    elif isinstance(text, str):
        return text.decode('utf-8').lower()
    else:
        raise TypeError('Expected string')
**Example Result:**
```python
def normalize_text(text):
    if isinstance(text, bytes):
        text = text.decode('utf-8')
    if isinstance(text, str):
        return text.lower()
    else:
        raise TypeError('Expected string or bytes')

JavaScript to TypeScript Migration Prompts

4. Gradual Typing with Strict Mode

Task: Convert a JavaScript module to TypeScript with strict: true in tsconfig.json, avoiding any where possible.

Prompt:

Convert this JavaScript code to TypeScript with `strict: true`. Follow these rules:
1. Add explicit type annotations for all function parameters and return types.
2. Replace `var` with `const` or `let` as appropriate.
3. Define interfaces for any object shape that appears more than once.
4. Use `unknown` instead of `any` for values whose type cannot be determined.
5. Use `as const` for literal types.
6. Do not use `// @ts-ignore` or `as any` without a comment explaining why.
7. Keep the same logic and structure.

JavaScript code:
```javascript
function calculateTotal(items) {
    let total = 0;
    for (let i = 0; i < items.length; i++) {
        total += items[i].price * items[i].quantity;
    }
    return total;
}

function formatUser(user) {
    return `${user.firstName} ${user.lastName} (${user.email})`;
}
**Example Result:**
```typescript
interface LineItem {
    price: number;
    quantity: number;
}

interface User {
    firstName: string;
    lastName: string;
    email: string;
}

function calculateTotal(items: LineItem[]): number {
    let total = 0;
    for (let i = 0; i < items.length; i++) {
        total += items[i].price * items[i].quantity;
    }
    return total;
}

function formatUser(user: User): string {
    return `${user.firstName} ${user.lastName} (${user.email})`;
}

5. Migrating Dynamic Objects with Generics

Task: Convert a JavaScript function that handles dynamic keys to TypeScript using generics and index signatures.

Prompt:

Convert this JavaScript utility to TypeScript. The function merges objects with dynamic keys. Use generics and index signatures to preserve type safety. Avoid `any`.

JavaScript code:
```javascript
function mergeObjects(a, b) {
    const result = {};
    for (let key in a) {
        if (a.hasOwnProperty(key)) {
            result[key] = a[key];
        }
    }
    for (let key in b) {
        if (b.hasOwnProperty(key)) {
            result[key] = b[key];
        }
    }
    return result;
}
**Example Result:**
```typescript
function mergeObjects<T extends Record<string, unknown>, U extends Record<string, unknown>>(a: T, b: U): T & U {
    const result = { ...a, ...b } as T & U;
    return result;
}

6. Migrating a Class-Based Module

Task: Convert an ES6 class with dynamic properties to TypeScript, ensuring property initialization and access modifiers.

Prompt:

Convert this JavaScript class to TypeScript. Add access modifiers (`private`, `public`, `protected`) where appropriate. Use parameter properties. Include proper typing for all methods and properties.

JavaScript code:
```javascript
class ApiClient {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
        this.cache = {};
    }

    async get(endpoint) {
        if (this.cache[endpoint]) {
            return this.cache[endpoint];
        }
        const response = await fetch(`${this.baseUrl}${endpoint}`);
        const data = await response.json();
        this.cache[endpoint] = data;
        return data;
    }
}
**Example Result:**
```typescript
interface CacheData {
    [endpoint: string]: unknown;
}

class ApiClient {
    private baseUrl: string;
    private cache: CacheData = {};

    constructor(baseUrl: string) {
        this.baseUrl = baseUrl;
    }

    async get<T>(endpoint: string): Promise<T> {
        if (this.cache[endpoint]) {
            return this.cache[endpoint] as T;
        }
        const response = await fetch(`${this.baseUrl}${endpoint}`);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data: T = await response.json();
        this.cache[endpoint] = data;
        return data;
    }
}

REST to GraphQL Migration Prompts

7. Generating a GraphQL Schema from REST Endpoints

Task: Given a set of REST endpoints, generate a GraphQL schema with queries and types that map to each endpoint.

Prompt:

You are migrating a REST API to GraphQL. Given the following REST endpoints and their responses, generate a GraphQL schema (using SDL) and resolver stubs. The schema should:
- Create a `Query` type with fields for each GET endpoint.
- Create a `Mutation` type for POST, PUT, DELETE endpoints.
- Use proper types and avoid `JSON` scalar unless necessary.
- Include pagination arguments (`first`, `after`) for list endpoints.

REST endpoints:
- GET /users  returns list of {id, name, email}
- GET /users/:id  returns single user
- POST /users  creates user, body: {name, email}
- GET /users/:id/posts  returns list of posts for user: {id, title, body}

Example Result:

# schema.graphql
type Query {
  users(first: Int, after: String): UserConnection!
  user(id: ID!): User
}

type Mutation {
  createUser(input: CreateUserInput!): User!
}

type User {
  id: ID!
  name: String!
  email: String!
  posts(first: Int, after: String): PostConnection!
}

type Post {
  id: ID!
  title: String!
  body: String!
}

type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

input CreateUserInput {
  name: String!
  email: String!
}

8. Writing Resolvers That Call Existing REST APIs

Task: Create GraphQL resolvers that internally call the existing REST API, acting as a gateway.

Prompt:

Write Apollo Server resolvers for the following GraphQL schema. Each resolver should call the corresponding REST endpoint using `fetch`. Handle errors by throwing a `GraphQLError`. Use DataLoader to batch requests where possible.

Schema:
- `Query.user(id: ID!): User`  calls GET /users/:id
- `Query.users(first: Int, after: String): UserConnection`  calls GET /users (with query params)
- `Mutation.createUser(input: CreateUserInput!): User`  calls POST /users

Example Result (JavaScript with Apollo Server):

const { GraphQLError } = require('graphql');
const DataLoader = require('dataloader');

const userLoader = new DataLoader(async (ids) => {
    const responses = await Promise.all(ids.map(id => fetch(`http://rest-api/users/${id}`)));
    return Promise.all(responses.map(r => r.json()));
});

const resolvers = {
    Query: {
        user: async (_, { id }) => {
            try {
                return await userLoader.load(id);
            } catch (error) {
                throw new GraphQLError('Failed to fetch user', { extensions: { code: 'REST_ERROR' } });
            }
        },
        users: async (_, { first, after }) => {
            const url = new URL('http://rest-api/users');
            if (first) url.searchParams.set('limit', first);
            if (after) url.searchParams.set('offset', after);
            const response = await fetch(url.toString());
            const data = await response.json();
            return {
                edges: data.map(user => ({ node: user, cursor: Buffer.from(user.id).toString('base64') })),
                pageInfo: {
                    hasNextPage: data.length === first,
                    endCursor: data.length ? Buffer.from(data[data.length - 1].id).toString('base64') : null
                }
            };
        }
    },
    Mutation: {
        createUser: async (_, { input }) => {
            const response = await fetch('http://rest-api/users', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(input)
            });
            if (!response.ok) {
                throw new GraphQLError('Failed to create user', { extensions: { code: 'REST_ERROR' } });
            }
            return response.json();
        }
    }
};

9. Migrating a REST Controller to GraphQL Resolver with N+1 Prevention

Task: Convert a REST endpoint that returns nested resources into a GraphQL resolver that avoids N+1 queries.

Prompt:

You have a REST controller that returns users with their posts in a single endpoint. In GraphQL, you want to split this into separate resolvers. Write resolvers for `User.posts` that use DataLoader to batch the database queries. Assume you have a `getPostsByUserIds(ids: number[]): Promise<Post[]>` function.

REST controller (for reference):
```javascript
app.get('/users-with-posts', async (req, res) => {
    const users = await db.users.findAll();
    for (let user of users) {
        user.posts = await db.posts.findAll({ where: { userId: user.id } });
    }
    res.json(users);
});
**Example Result:**
```javascript
const DataLoader = require('dataloader');

const postsByUserLoader = new DataLoader(async (userIds) => {
    const posts = await getPostsByUserIds(userIds);
    const grouped = userIds.map(id => posts.filter(post => post.userId === id));
    return grouped;
});

const resolvers = {
    User: {
        posts: async (parent) => {
            return postsByUserLoader.load(parent.id);
        }
    }
};

10. Adding Mutations with Input Validation

Task: Create a GraphQL mutation that replaces a REST POST endpoint, including input validation.

Prompt:

Write a GraphQL mutation `createPost` that replaces the REST endpoint `POST /posts`. The resolver should:
1. Validate input (title must be non-empty, body max 1000 chars).
2. Call a function `db.createPost(data)` that returns the new post.
3. Return the created post or throw a `UserInputError` with validation details.
4. Include the mutation definition in SDL.

REST equivalent:
```javascript
app.post('/posts', (req, res) => {
    const { title, body, userId } = req.body;
    if (!title || title.trim() === '') {
        return res.status(400).json({ error: 'Title is required' });
    }
    if (body.length > 1000) {
        return res.status(400).json({ error: 'Body too long' });
    }
    const post = db.createPost({ title, body, userId });
    res.status(201).json(post);
});
**Example Result:**

```graphql
# SDL
type Mutation {
  createPost(input: CreatePostInput!): Post!
}

input CreatePostInput {
  title: String!
  body: String!
  userId: ID!
}
// Resolver
const { UserInputError } = require('apollo-server-express');

const resolvers = {
    Mutation: {
        createPost: async (_, { input }) => {
            const errors = [];
            if (!input.title || input.title.trim() === '') {
                errors.push({ field: 'title', message: 'Title is required' });
            }
            if (input.body.length > 1000) {
                errors.push({ field: 'body', message: 'Body must be at most 1000 characters' });
            }
            if (errors.length > 0) {
                throw new UserInputError('Validation failed', { invalidArgs: errors });
            }
            const post = await db.createPost({ title: input.title, body: input.body, userId: input.userId });
            return post;
        }
    }
};

Practical Tips for Using These Prompts

  1. Iterate incrementally. Don't migrate your entire codebase in one prompt. Start with small modules, verify correctness, then expand.
  2. Use diffs for review. Always review AI-generated code changes using a diff tool (like GitHub's pull request review). AI can hallucinate function signatures or introduce subtle regressions.
  3. Combine with static analysis. For Python 2→3, run pylint --py3k or modernize first. For JS→TS, use the TypeScript compiler's --allowJs and --checkJs flags incrementally.
  4. Pin AI model versions. The same prompt can produce different results across model updates. If you rely on an AI for migrations, note the model version in your documentation.

Conclusion

Code migration is a high-risk, high-reward activity. The prompts above give you a structured way to leverage AI assistants for the mechanical parts of migration—syntax translation, type annotation, schema generation—while you focus on the strategic decisions: which legacy patterns to keep, which to refactor, and how to test the result.

Remember that no prompt replaces a comprehensive test suite. After applying any of these prompts, run your existing unit tests, integration tests, and ideally a canary deployment before full rollout. With careful prompting and rigorous validation, you can cut migration time significantly without sacrificing code quality.

← All posts

Comments