Working with Generated Code

15 minpublished

Develop workflows for reviewing, understanding, and integrating AI-generated code into your projects.

Working with Generated Code

You've just asked your AI assistant to generate a complex function, and it delivered—200 lines of code that looks right. Now what? The gap between receiving generated code and actually shipping it to production is where many developers stumble. This lesson teaches you how to effectively work with AI-generated code to build reliable, maintainable software.

The Reality of AI-Generated Code

AI assistants are powerful coding partners, but they're not magical. Generated code arrives with its own set of considerations. Understanding these upfront will save you hours of debugging and refactoring.

What AI Does Well

  • Boilerplate and patterns: CRUD operations, standard API endpoints, common data transformations
  • Familiar algorithms: Sorting, searching, parsing well-known formats
  • Type-safe structures: Interface definitions, schema objects, configuration types
  • Standard integrations: Database connections, HTTP clients, authentication flows using established libraries

Where AI Needs Your Guidance

AI struggles with:

  • Your specific business logic: Domain-specific rules that aren't documented publicly
  • Performance-critical sections: May choose readable over optimal approaches
  • Complex state management: Multi-step workflows with intricate dependencies
  • Security edge cases: May miss authentication checks or input validation in specific scenarios

Knowing these boundaries helps you prompt effectively and know where to focus your review efforts. For deeper insights on spotting AI limitations, check out hallucination-detection.

The Integration Workflow

Here's a systematic approach to working with generated code that minimizes risk while maximizing productivity.

Step 1: Read Before Running

Never blindly accept generated code. Scan it first:

# AI Generated: User authentication function
async def authenticate_user(username: str, password: str) -> User:
    user = await db.get_user(username)
    if user and user.password == password:  # đźš© Red flag!
        return user
    raise AuthenticationError("Invalid credentials")

Red flags to watch for:

  • Plain text password comparison (like above)
  • Missing error handling
  • Hardcoded values
  • Deprecated APIs or libraries
  • Inefficient database queries (N+1 problems)

Even a 30-second scan catches obvious issues. This is your first line of defense.

Step 2: Test Incrementally

Don't integrate large chunks of generated code all at once. Build incrementally:

// AI generated a complete payment processing module
// DON'T: Drop the entire 500-line module into your codebase
// DO: Integrate piece by piece

// First: Just the payment validation
function validatePaymentData(paymentInfo) {
  if (!paymentInfo.amount || paymentInfo.amount <= 0) {
    throw new Error('Invalid amount');
  }
  // ... more validation
  return true;
}

// Test this first
test('validates payment amounts', () => {
  expect(() => validatePaymentData({ amount: -10 }))
    .toThrow('Invalid amount');
});

// Then: Add the processing logic
// Then: Add error handling
// Finally: Add logging and monitoring

Incremental integration lets you:

  • Identify issues quickly
  • Understand each component
  • Build confidence progressively
  • Make targeted fixes

For comprehensive testing approaches, see testing-strategies.

Step 3: Refactor for Your Context

AI generates code in a vacuum. You need to adapt it:

// AI Generated: Generic user service
class UserService {
  async getUser(id: string): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  }
}

// Refactored: Matches your architecture
class UserService {
  constructor(private apiClient: ApiClient, private cache: Cache) {}
  
  async getUser(id: string): Promise<User> {
    // Use your existing API client
    const cached = await this.cache.get(`user:${id}`);
    if (cached) return cached;
    
    // Matches your error handling patterns
    const user = await this.apiClient.get<User>(`/users/${id}`);
    await this.cache.set(`user:${id}`, user, { ttl: 300 });
    return user;
  }
}

Refactoring considerations:

  • Use your abstractions: Plug into existing services, utilities, and patterns
  • Match your error handling: Don't mix error handling styles
  • Apply your naming conventions: Stay consistent with your codebase
  • Consider your architecture: Respect layer boundaries and dependencies

Learn more about systematic refactoring in review-refactor.

Managing Dependencies and Imports

AI assistants often suggest packages without considering your constraints.

Version Compatibility

# AI suggested code using pandas 2.0 features
import pandas as pd

df = pd.read_csv('data.csv', dtype_backend='numpy_nullable')  # Pandas 2.0+

# But your project is locked to pandas 1.5
# Check your requirements.txt first!
# Adapt or upgrade consciously

Action items:

  1. Check if suggested packages are already in your project
  2. Verify version compatibility with your stack
  3. Assess the security and maintenance status of new dependencies
  4. Consider lighter alternatives for simple use cases

Avoiding Dependency Bloat

// AI suggests: Use lodash for array operations
import _ from 'lodash';

const unique = _.uniq(items);
const sorted = _.sortBy(users, 'name');

// Consider: Native alternatives (if they fit your needs)
const unique = [...new Set(items)];
const sorted = [...users].sort((a, b) => a.name.localeCompare(b.name));

// Or: Import specific functions to reduce bundle size
import uniq from 'lodash/uniq';
import sortBy from 'lodash/sortBy';

Every dependency adds weight, security surface, and maintenance burden. Be deliberate.

Debugging Generated Code

When generated code doesn't work as expected, systematic debugging saves time.

Start with Inputs and Outputs

def process_user_data(users):
    """AI-generated function that's failing"""
    result = []
    for user in users:
        if user['active'] and user['age'] >= 18:
            result.append({
                'id': user['id'],
                'name': f"{user['first']} {user['last']}",
                'email': user['email'].lower()
            })
    return result

# Debug: What inputs cause failures?
test_cases = [
    [],  # Empty list
    [{'id': 1}],  # Missing keys
    [{'id': 1, 'active': True, 'age': 20, 'first': 'John'}],  # Missing 'last'
    [{'id': 1, 'active': True, 'age': 20, 'first': 'John', 'last': None}],  # None values
]

for case in test_cases:
    try:
        result = process_user_data(case)
        print(f"âś“ {case} -> {result}")
    except Exception as e:
        print(f"âś— {case} failed: {e}")

Systematic input testing reveals assumptions the AI made about your data structure.

Add Strategic Logging

// AI-generated complex workflow
func ProcessOrder(order Order) error {
    // Add logging to understand flow
    log.Printf("Processing order %s, status: %s", order.ID, order.Status)
    
    if err := validateOrder(order); err != nil {
        log.Printf("Validation failed for order %s: %v", order.ID, err)
        return err
    }
    
    inventory := checkInventory(order.Items)
    log.Printf("Inventory check for order %s: %+v", order.ID, inventory)
    
    // ... rest of function
}

Logging helps you:

  • Trace execution flow
  • Identify where assumptions break
  • Understand state at each step
  • Catch silent failures

Use Your IDE's Tools

Generated code isn't special—debug it like any code:

  • Set breakpoints at key decision points
  • Step through loops to watch state changes
  • Inspect variable values at each step
  • Use your debugger's conditional breakpoints for specific scenarios

Code Quality and Standards

Generated code should meet the same standards as hand-written code.

Apply Your Linters and Formatters

# After accepting generated code
npm run lint:fix
npm run format

# Or for Python
black src/
flake8 src/
mypy src/

AI might not match your team's style perfectly. Automation fixes this instantly. For organizational approaches, see team-workflows.

Security Review Checklist

Before committing generated code, verify:

  • Input validation: All external input is validated and sanitized
  • Authentication checks: Protected endpoints require proper auth
  • SQL injection prevention: Parameterized queries, no string concatenation
  • XSS protection: User content is properly escaped
  • Secrets management: No hardcoded credentials or API keys
  • Error messages: Don't leak sensitive information
# AI Generated - BEFORE security review
@app.route('/user/<user_id>')
def get_user(user_id):
    query = f"SELECT * FROM users WHERE id = {user_id}"  # SQL injection!
    return db.execute(query)

# AFTER security review
@app.route('/user/<user_id>')
@require_authentication
def get_user(user_id):
    # Validate input
    if not user_id.isdigit():
        return {"error": "Invalid user ID"}, 400
    
    # Parameterized query
    query = "SELECT id, name, email FROM users WHERE id = ?"
    user = db.execute(query, (user_id,))
    
    if not user:
        return {"error": "User not found"}, 404
    
    return user

Dive deeper into this critical topic in security-considerations.

Documentation and Comments

AI-generated code often comes with comments. Treat them critically.

Update Generated Comments

// AI Generated comment - generic and possibly wrong
/**
 * Fetches user data from the API
 * @param {string} id - User identifier
 * @returns {Promise<User>} User object
 */
async function getUser(id) {
  const user = await cache.get(`user:${id}`);
  if (user) return user;
  
  const response = await api.get(`/users/${id}`);
  await cache.set(`user:${id}`, response, { ttl: 300 });
  return response;
}

// Better comment after understanding the code
/**
 * Retrieves user data with 5-minute cache
 * Falls back to API if cache miss
 * @param {string} id - UUID of user
 * @returns {Promise<User>} User with profile data
 * @throws {ApiError} When user not found or API unavailable
 */

Comments should reflect what the code actually does in your context, not what the AI thought it would do.

Document Integration Points

// When integrating AI-generated modules, document the boundaries

/**
 * Payment processing service
 * 
 * GENERATED: Core validation and processing logic (AI-assisted)
 * CUSTOMIZED: Error handling, logging, and webhook integration
 * 
 * Integrates with:
 * - Stripe API (via PaymentGateway service)
 * - OrderService for status updates
 * - NotificationService for customer emails
 * 
 * Security: All amounts validated, PCI-compliant (no card data stored)
 */
class PaymentService {
  // ...
}

This helps future you (and teammates) understand what's generated versus customized. More on this in doc-generation.

Version Control Best Practices

How you commit generated code matters.

Commit Strategy

# DON'T: Single massive commit
git add .
git commit -m "Added payment processing"

# DO: Separate commits for clarity
git add src/payments/validation.ts
git commit -m "Add payment validation logic (AI-generated, reviewed)"

git add src/payments/processing.ts
git commit -m "Add payment processing with Stripe integration"

git add tests/payments/
git commit -m "Add payment processing tests"

git add src/payments/processing.ts  # After refactoring
git commit -m "Refactor: Add error handling and logging to payment processing"

Smaller commits make it easier to:

  • Review changes
  • Revert specific pieces if needed
  • Understand the evolution through git history
  • Collaborate with teammates

Learn more in version-control-ai.

Marking Generated Code

Consider tagging in commit messages:

git commit -m "feat: Add user authentication service

AI-assisted generation with manual security review and refactoring.
Generated base structure, customized error handling and logging."

This provides context for code archaeology later.

Performance Optimization

AI prioritizes correctness over performance. You need to optimize.

Profile Before Optimizing

# AI-generated data processing
def process_records(records):
    results = []
    for record in records:
        # Multiple database calls per record
        user = db.get_user(record['user_id'])
        category = db.get_category(record['category_id'])
        results.append({
            'user': user.name,
            'category': category.name,
            'amount': record['amount']
        })
    return results

# Profile it first
import time
start = time.time()
result = process_records(test_data)
print(f"Processed {len(test_data)} records in {time.time() - start:.2f}s")

# Then optimize (batch queries)
def process_records_optimized(records):
    user_ids = {r['user_id'] for r in records}
    category_ids = {r['category_id'] for r in records}
    
    users = {u.id: u for u in db.get_users_batch(user_ids)}
    categories = {c.id: c for c in db.get_categories_batch(category_ids)}
    
    return [{
        'user': users[r['user_id']].name,
        'category': categories[r['category_id']].name,
        'amount': r['amount']
    } for r in records]

Measure, optimize, measure again. See performance-optimization for deeper techniques.

Building Iteration Loops

Working with AI is iterative. Embrace it.

The Refinement Cycle

  1. Generate: Get initial code
  2. Review: Identify issues and improvements
  3. Refine prompt: Ask AI to fix specific issues
  4. Test: Verify the changes
  5. Repeat: Until it meets your standards
# Iteration 1: Basic function
# Prompt: "Write a function to calculate shipping cost"
def calculate_shipping(weight):
    return weight * 0.5

# Iteration 2: Add complexity
# Prompt: "Add support for different shipping zones and express delivery"
def calculate_shipping(weight, zone, is_express=False):
    base_rate = {'domestic': 0.5, 'international': 1.5}
    cost = weight * base_rate[zone]
    return cost * 1.5 if is_express else cost

# Iteration 3: Add validation and error handling
# Prompt: "Add input validation and handle edge cases"
def calculate_shipping(weight, zone, is_express=False):
    if weight <= 0:
        raise ValueError("Weight must be positive")
    
    base_rates = {'domestic': 0.5, 'international': 1.5}
    if zone not in base_rates:
        raise ValueError(f"Invalid zone: {zone}")
    
    cost = weight * base_rates[zone]
    return cost * 1.5 if is_express else cost

Each iteration builds on the previous. This is faster than trying to perfect the prompt upfront.

Common Pitfalls and Solutions

Pitfall 1: Accepting Without Understanding

Problem: Code works but you don't know how or why.

Solution: Take 5 minutes to trace through the logic. Add comments explaining key steps. If you can't explain it, you can't maintain it.

Pitfall 2: Over-Engineering

Problem: AI generates overly complex solutions for simple problems.

// AI might generate this for simple string formatting
class StringFormatter {
  constructor(template) {
    this.template = template;
    this.variables = new Map();
  }
  
  setVariable(key, value) {
    this.variables.set(key, value);
    return this;
  }
  
  format() {
    let result = this.template;
    this.variables.forEach((value, key) => {
      result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
    });
    return result;
  }
}

// When you just needed
const formatString = (template, vars) => 
  Object.entries(vars).reduce(
    (str, [key, val]) => str.replace(`{${key}}`, val),
    template
  );

Solution: Ask "Is this complexity necessary?" Simplify aggressively.

Pitfall 3: Ignoring Edge Cases

Problem: Generated code handles the happy path only.

Solution: Always test with:

  • Empty inputs
  • Null/undefined values
  • Extremely large inputs
  • Invalid data types
  • Boundary conditions

See quality-control for comprehensive approaches.

Practical Exercise

Try this workflow with your next AI-generated function:

  1. Generate a data validation function for a form
  2. Review for missing validations
  3. Test with 5 different invalid inputs
  4. Refactor to match your error handling pattern
  5. Document the validation rules
  6. Commit with clear message about what's generated vs. customized

Time yourself. With practice, this process becomes second nature.

Key Takeaways

  • Never trust blindly: Always review generated code before integration
  • Test incrementally: Integrate piece by piece, not in large chunks
  • Refactor to fit: Adapt generated code to your architecture and patterns
  • Maintain standards: Generated code must meet the same quality bar
  • Iterate freely: Multiple refinement cycles are faster than perfect prompts
  • Document intent: Mark what's generated and what's customized
  • Measure performance: AI prioritizes correctness; you optimize

Working effectively with generated code is a skill. The more you practice this systematic approach, the faster and more confidently you'll ship AI-assisted code to production. You're not just accepting what the AI gives you—you're collaborating with it to build production-ready software.

Next, level up your skills with component-generation to build larger architectural pieces with AI assistance, and explore managing-tech-debt to keep your AI-assisted codebase healthy long-term.