Refyne
All articles
EngineeringMay 21, 2026·7 min read

5 Things We Always Find in AI-Generated Codebases (And How to Fix Them)

We've reviewed dozens of AI-assisted SaaS codebases. The good ones ship fast. The bad ones also ship fast — and then fall apart six months later when a security audit, a scaling event, or a new hire opens the hood.

After enough of these reviews, you start to see the same patterns. Here are the five that appear most consistently, along with the fix for each.

No Input Validation on API Routes

AI-generated backends tend to trust their input completely. The code works in the happy path, but the sad paths are empty.

What we find
export async function POST(req: Request) {
  const { email, planId } = await req.json()
  await db.createSubscription({ email, planId })
  return Response.json({ ok: true })
}
What it should look like
import { z } from 'zod'

const schema = z.object({
  email: z.string().email(),
  planId: z.enum(['starter', 'pro', 'enterprise']),
})

export async function POST(req: Request) {
  const result = schema.safeParse(await req.json())
  if (!result.success) {
    return Response.json(
      { error: result.error.flatten() },
      { status: 400 }
    )
  }
  await db.createSubscription(result.data)
  return Response.json({ ok: true })
}

One unvalidated string field is a SQL injection vector. One unvalidated enum is a privilege escalation waiting to happen.

Secrets Hardcoded or Stored Adjacent to the Repo

LLMs are trained to be helpful. That means they'll happily accept a hardcoded API key in a prompt and write code that uses it directly. We regularly find:

  • Stripe secret keys in .env.example (committed to git)
  • API keys in comments explaining what the variable “should be”
  • Supabase service role keys hardcoded in a utility file

The fix isn't complicated: rotate whatever was exposed, then store everything in a proper secrets manager or at minimum .env.local (gitignored) with runtime injection in CI/CD. But catching it requires actually auditing the commit history — not just the current file tree.

Missing Error Boundaries in React Apps

React renders are optimistic. Without error boundaries, a single unhandled exception anywhere in the tree unmounts the entire app — your customer sees a blank screen, not an error message.

No error boundary — full app crashes
export default function Dashboard() {
  const data = useSomeDataThatMightFail()
  return <ComplexUI data={data} />
}
Wrap with a boundary and a fallback
import { ErrorBoundary } from 'react-error-boundary'

export default function Dashboard() {
  return (
    <ErrorBoundary
      fallback={
        <div>
          Something went wrong.{' '}
          <a href="/">Reload</a>
        </div>
      }
    >
      <ComplexUI />
    </ErrorBoundary>
  )
}

The fix takes ten minutes. The blast radius without it is your entire product going dark.

Zero Test Coverage on Business Logic

AI tools write test files when you ask for them. They don't write them when you don't. The result is coverage that's cosmetic at best: a few passing smoke tests on the happy path, nothing on the edge cases that matter.

The patterns we see most:

  • No tests on billing code (refunds, upgrades, dunning)
  • No tests on auth flows (token expiry, permission checks)
  • Integration tests that mock the database (which defeats the purpose)

The fix isn't “write more tests.” It's identifying which 20% of your code is responsible for 80% of your business risk and starting there.

N+1 Queries from AI-Generated ORM Code

ORMs abstract the database nicely. Too nicely. AI-generated code tends to iterate over a collection and call the database inside the loop without realising it.

N+1: one query per user
const users = await db.user.findMany()

for (const user of users) {
  const plan = await db.plan.findUnique({
    where: { id: user.planId },
  })
  console.log(user.name, plan.name)
}
One query with a join
const users = await db.user.findMany({
  include: { plan: true },
})

for (const user of users) {
  console.log(user.name, user.plan.name)
}

At ten users, this is invisible. At ten thousand, it crashes your database. We've seen apps go down at their first press mention because of this exact pattern.

What to Do Next

These five patterns are a starting point, not a complete picture. In most AI-generated codebases, they compound — a missing error boundary hides a broken API call that's leaking secrets that aren't rotated because there are no tests to catch the regression.

Free Codebase Review

If any of these patterns look familiar, our free codebase review will find the rest.

We'll map the failure patterns, rank them by risk, and give you a written breakdown — no sales call required.

Request a free reviewFree — no sales call required