The Gap Between AI Code and Production Code
AI assistants write code that compiles and runs. That's not the same as code that's ready for production.
Here's what to check every time you ship AI-generated code.
Check 1: Error Handling
// AI often generates this:
const data = await fetch('/api/data').then(r => r.json())
// Production requires:
async function fetchData() {
const res = await fetch('/api/data')
if (!res.ok) {
throw new Error(`API error: ${res.status} ${res.statusText}`)
}
return res.json()
}
// In the component:
try {
const data = await fetchData()
} catch (error) {
logger.error({ error }, 'Failed to fetch data')
// Show user-friendly error, don't crash
}
Check 2: Input Validation
// AI often skips validation:
export async function POST(req: Request) {
const { email, name } = await req.json()
await db.user.create({ data: { email, name } }) // No validation!
}
// Production:
import { z } from 'zod'
const Schema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100)
})
export async function POST(req: Request) {
const body = await req.json()
const result = Schema.safeParse(body)
if (!result.success) {
return Response.json({ error: result.error.issues }, { status: 400 })
}
await db.user.create({ data: result.data })
}
Check 3: Authentication
// AI generates the handler but often forgets auth:
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
await db.post.delete({ where: { id: params.id } }) // Anyone can delete!
}
// Production:
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
const session = await getServerSession(authOptions)
if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 })
// Verify ownership -- can this user delete this post?
const post = await db.post.findUnique({ where: { id: params.id } })
if (post?.userId !== session.user.id) {
return Response.json({ error: 'Forbidden' }, { status: 403 })
}
await db.post.delete({ where: { id: params.id } })
return Response.json({ success: true })
}
Check 4: Race Conditions
// AI often generates non-atomic operations:
const user = await db.user.findUnique({ where: { id } })
if (user.credits > 0) {
await db.user.update({ where: { id }, data: { credits: user.credits - 1 } })
await processRequest(id)
}
// Race condition: two simultaneous requests both see credits > 0
// Production -- atomic with transaction:
await db.$transaction(async (tx) => {
const user = await tx.user.findUniqueOrThrow({ where: { id } })
if (user.credits <= 0) throw new Error('Insufficient credits')
await tx.user.update({ where: { id }, data: { credits: { decrement: 1 } } })
await processRequest(id, tx)
})
Check 5: N+1 Queries
// AI generates queries in loops:
const users = await db.user.findMany()
const usersWithPosts = await Promise.all(
users.map(async user => ({
...user,
posts: await db.post.findMany({ where: { userId: user.id } })
// N additional queries for N users
}))
)
// Production -- single query with include:
const usersWithPosts = await db.user.findMany({
include: { posts: true } // One query with JOIN
})
The Review Prompt
Before shipping any AI-generated code:
Claude Code: 'Review this code I just generated for:
1. Missing error handling
2. Missing input validation
3. Missing authentication checks
4. Race conditions
5. N+1 database queries
6. Hardcoded values that should be environment variables
List specific issues. Skip style feedback.'
The MCP Security Scanner Does This for MCP Code
The same categories of issues appear in MCP server code -- often worse, because MCP server authors aren't always security-focused web developers.
$29/mo at whoffagents.com
Top comments (0)