The Demo-to-Production Gap
AI-generated code that works in a demo often fails in production for predictable reasons:
- Missing error handling (happy path only)
- No input validation
- Security vulnerabilities (SQL injection, missing auth, secret exposure)
- No consideration for edge cases or concurrent access
- No logging or observability
- Hard-coded values that should be configuration
The gap between demo code and production code is not about correctness — it is about defensive programming, security, observability, and resilience. This chapter shows you how to get production-ready code from AI by asking for it explicitly.
---
The Production Code Prompt Framework
The key insight: AI generates code at the quality level implied by the prompt. A prompt that does not mention error handling gets code without error handling. A prompt that explicitly requires production standards gets production-quality code.
The Production Code System Prompt
code
You write production-quality code by default. This means:
MANDATORY FOR ALL CODE:
- Every async operation wrapped in try/catch with appropriate error handling
- All user inputs validated before use (type, range, required fields)
- Meaningful error messages that distinguish different failure modes
- Structured logging for operations that matter
- Constants for magic values (no raw strings or numbers that will change)
SECURITY DEFAULTS:
- Parameterised queries for all database operations
- Authentication check before any sensitive operation
- Authorisation check (not just authentication -- user X should not access user Y's data)
- No secrets, credentials, or sensitive data in logs or responses
- Rate limiting recommendation on any public endpoint
NEVER:
- Silent catch blocks (catch(err) {} with no action)
- Broad exception swallowing (catch(err) { return null } without logging)
- Unchecked type assumptions (assuming req.body.id is a number without validation)
---
Error Handling Patterns
The most common production failure in AI-generated code is inadequate error handling.
The pattern AI typically generates:
javascript
async function getUser(userId) {
const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`);
return user[0];
}
Problems: SQL injection, no error handling, silent undefined return.
Prompt for production-quality error handling:
code
Write a getUser function. Requirements:
- Parameterised query (never interpolate userId directly)
- Explicit handling for: user not found (null/404 scenario), database connection error, invalid userId format
- Typed return: User | null (with a custom UserNotFoundError vs DatabaseError distinction)
- Logging for errors at appropriate severity levels
- Input validation: userId must be a non-empty string matching UUID format
Production output:
typescript
class UserNotFoundError extends Error {
constructor(userId) {
super('User not found: ' + userId);
this.name = 'UserNotFoundError';
}
}
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
async function getUser(userId) {
if (!userId || !UUID_REGEX.test(userId)) {
throw new Error('Invalid userId format: ' + userId);
}
try {
const result = await db.query(
'SELECT id, email, created_at FROM users WHERE id = $1',
[userId] // Parameterised -- never interpolated
);
if (result.rows.length === 0) {
return null; // User not found -- not an error
}
return result.rows[0];
} catch (err) {
logger.error('Database error in getUser', { userId, error: err.message });
throw new Error('Failed to retrieve user due to database error');
}
}
---
The Security Review Prompt
After generating any API route or data access function, always run this review prompt:
code
Security review this code. For each issue found, classify severity and provide the exact fix.
Check specifically for:
1. Authentication: Is the user's identity verified before any data access?
2. Authorisation: Can user A access user B's data through this code?
3. Injection: Is any user-controlled data interpolated into queries, commands, or evaluated code?
4. Secrets: Are any API keys, passwords, or tokens visible in the code or return values?
5. Rate limiting: Could this endpoint be abused at scale?
6. IDOR: Do object IDs in URLs/params get validated against the authenticated user's ownership?
7. Input validation: Are all inputs validated for type, length, format, and range before use?
Code to review:
[paste code]
---
Testing Integration From the Start
The most efficient time to write tests is immediately after writing the code, while context is fresh.
code
I have just written this function:
[paste function]
Now write Jest tests for it. Cover:
1. Happy path with valid inputs -- all return paths
2. Each validation failure (invalid types, missing required fields, out-of-range values)
3. Error handling paths -- what happens when the database throws?
4. Edge cases specific to this function's logic
For each test:
- Descriptive test name that explains what is being tested
- Clear arrange/act/assert structure
- Mock external dependencies (database, external APIs) -- do not make real calls
---
The Observability Prompt
Production code needs to be debuggable when things go wrong.
code
Add structured logging to this function. Log:
- Function entry with key parameters (redact any PII or secrets)
- Each major decision point
- All errors with full context
- Performance metrics for operations over 100ms
Use our logger utility: logger.info(), logger.warn(), logger.error()
Each log call should include: { event: 'descriptive_name', ...relevant_context }
---
Configuration vs. Hard-Coding
code
Add this rule to every code generation prompt:
"Replace all hard-coded values (URLs, timeouts, limits, feature flags, magic numbers) with named constants defined at the top of the file. Where a value should be configurable per environment, use process.env.VARIABLE_NAME with a documented default."