Working with AI-Generated Code: A Developer's Guide
# 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](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:
```python
# 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:
```javascript
// 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](testing-strategies).
### Step 3: Refactor for Your Context
AI generates code in a vacuum. You need to adapt it:
```typescript
// AI Generated: Generic user service
class UserService {
async getUser(id: string): Promise {
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 {
// 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(`/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](review-refactor).
## Managing Dependencies and Imports
AI assistants often suggest packages without considering your constraints.
### Version Compatibility
```python
# 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
```javascript
// 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
```python
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
```go
// 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
```bash
# 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](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
```python
# AI Generated - BEFORE security review
@app.route('/user/')
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/')
@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](security-considerations).
## Documentation and Comments
AI-generated code often comes with comments. Treat them critically.
### Update Generated Comments
```javascript
// AI Generated comment - generic and possibly wrong
/**
* Fetches user data from the API
* @param {string} id - User identifier
* @returns {Promise} 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 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
```typescript
// 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](doc-generation).
## Version Control Best Practices
How you commit generated code matters.
### Commit Strategy
```bash
# 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](version-control-ai).
### Marking Generated Code
Consider tagging in commit messages:
```bash
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
```python
# 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](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
```python
# 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.
```javascript
// 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](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](component-generation) to build larger architectural pieces with AI assistance, and explore [managing-tech-debt](managing-tech-debt) to keep your AI-assisted codebase healthy long-term.